Privy Digital ID — Integration GuidePanduan Integrasi
Privy's official identity verification service, embedded by merchants via an iframe (web) or WebView (native app). Merchants simply create a session and receive the verified result — the entire phone, liveness, and face-match flow runs inside Privy's widget. Layanan verifikasi identitas resmi Privy, di-embed merchant lewat iframe (web) atau WebView (aplikasi native). Merchant cukup membuat sesi dan menerima hasil terverifikasi — seluruh alur input nomor, liveness, dan pencocokan wajah dijalankan Privy di dalam widget.
EnvironmentsEnvironment
Two environments, same API. Use Sandbox for integration and testing, Production for live traffic — just swap the base host. Dua environment, API sama. Pakai Sandbox untuk integrasi & pengujian, Production untuk trafik live — cukup ganti base host.
| Environment | API base URLBase URL API | Widget base URLBase URL widget |
|---|---|---|
| Sandbox | https://sandbox-api-digitalid.privy.id | https://sandbox-digitalid.privy.id |
| Production | https://api-digitalid.privy.id | https://digitalid.privy.id |
OverviewRingkasan
Privy Digital ID is an identity verification widget. The merchant never handles the camera, biometric data, or the user's personal data — the merchant only: Privy Digital ID adalah widget verifikasi identitas. Merchant tidak menangani kamera, data biometrik, maupun data pribadi pengguna — merchant hanya:
- Creates a session from its backend (with an API key) → receives a
session_url. Membuat sesi dari backend (dengan API key) → menerimasession_url. - Embeds the
session_urlin an iframe / WebView. Meng-embedsession_urldi iframe / WebView. - Receives the result via a server-to-server callback (source of truth) and/or a postMessage event. Menerima hasil lewat callback server-to-server (sumber kebenaran) dan/atau event postMessage.
How it worksBagaimana ia bekerja
There are two parties: the Merchant (who embeds) and Privy (who verifies identity). The whole process runs inside Privy's widget. Ada dua pihak: Merchant (yang meng-embed) dan Privy (yang memverifikasi identitas). Seluruh proses berjalan di dalam widget Privy.
| StepLangkah | What Privy doesYang dilakukan Privy |
|---|---|
| 1 · PhoneNomor HP | The user enters a phone number; Privy matches it against a registered identity.Pengguna memasukkan nomor; Privy mencocokkannya dengan identitas terdaftar. |
| 2 · Liveness | Face-liveness detection via camera (anti-spoof, gestures, light reflection).Deteksi keaslian wajah lewat kamera (anti-spoof, gerakan, refleksi cahaya). |
| 3 · Face matchPencocokan wajah | The live selfie is matched against the face on the verified Privy identity.Selfie hidup dicocokkan dengan wajah pada identitas terverifikasi Privy. |
| 4 · ConsentPersetujuan | The user reviews the data to share, then agrees or declines.Pengguna meninjau data yang akan dibagikan lalu menyetujui / menolak. |
| 5 · ResultHasil | Privy delivers the verified data to the merchant (callback + exchange code).Privy mengirim data terverifikasi ke merchant (callback + exchange code). |
Integration flowDiagram integrasi
The full sequence between the four parties. Steps 1–3 set up the widget; step 4 all happens inside Privy; steps 5–8 deliver the verified data to the merchant. Urutan lengkap antara empat pihak. Langkah 1–3 menyiapkan widget; langkah 4 seluruhnya terjadi di dalam Privy; langkah 5–8 mengantar data terverifikasi ke merchant.
Quickstart
1 · Create a session (merchant backend)Buat sesi (backend merchant)
# API key stays in the backend — never in the browserAPI key hanya di backend — jangan pernah di browser
curl -X POST https://sandbox-api-digitalid.privy.id/v1/sessions \
-H "X-API-Key: <MERCHANT_API_KEY>"
{
"session_id": "9f1c…",
"session_url": "https://sandbox-digitalid.privy.id/?t=eyJ…",
"expires_at": "2026-07-21T18:15:00Z"
}
2 · Embed the widget (merchant frontend)Embed widget (frontend merchant)
<iframe src="{{session_url}}" allow="camera"
style="width:100%;height:100%;border:0"></iframe>
3 · Receive the resultTerima hasil
window.addEventListener('message', (e) => {
if (e.origin !== 'https://sandbox-digitalid.privy.id') return; // requiredwajib
const { type, payload } = e.data || {};
if (type === 'privy:success') {
// hand payload.exchange_code to your backend to swap for a tokenserahkan payload.exchange_code ke backend untuk ditukar token
}
});
Verification statesAlur verifikasi
The state machine is enforced server-side. Data-sharing consent happens AFTER the face match (Privy's regulation): State machine dijaga server-side. Consent berbagi data terjadi SETELAH wajah cocok (regulasi Privy):
| State | ScreenLayar | MeaningArti |
|---|---|---|
created | Phone inputInput nomor HP | Session createdSesi dibuat |
identity_found | Liveness (camera)Liveness (kamera) | Phone matched → liveness + face match run right awayNomor cocok → liveness + face match langsung berjalan |
verified | Consent screenLayar consent | Face matched → the user reviews & agrees/declinesWajah cocok → user meninjau & menyetujui/menolak |
completed | SuccessSukses | Agreed → privy:successDisetujui → privy:success |
identity_not_found | Phone (error)Input nomor (error) | Number not registered → retryNomor tidak terdaftar → coba lagi |
consent_denied | CancelledDibatalkan | User declined → privy:cancelUser menolak → privy:cancel |
blocked | BlockedTerblokir | 3 liveness failures → privy:errorGagal liveness 3× → privy:error |
AuthenticationAutentikasi
Privy issues one API key per merchant. All back-channel requests (create session, exchange
code) carry the X-API-Key header.
Privy menerbitkan satu API key untuk tiap merchant. Semua permintaan back-channel (buat sesi,
tukar exchange code) dikirim dengan header X-API-Key.
| CredentialKredensial | Header | Used forDipakai untuk |
|---|---|---|
| Merchant API Key | X-API-Key | Create session & exchange code — from the merchant backend onlyBuat sesi & tukar exchange code — dari backend merchant saja |
X-API-Key in browser code. Sessions are always created from the merchant backend.
Jangan pernah menaruh X-API-Key di kode browser. Sesi selalu dibuat dari backend merchant.
API Reference
Creates a verification session and returns the URL the merchant embeds. No body. Membuat sesi verifikasi dan mengembalikan URL yang di-embed merchant. Tanpa body.
ResponseRespons 201
{ "session_id":"…", "session_url":"https://sandbox-digitalid.privy.id/?t=…", "expires_at":"…Z" }
Swaps the exchange code (received on the frontend via privy:success) for the
verified-data token. The code is single-use and short-lived (~5 min); it is worthless without the API key.
Menukar exchange code (diterima frontend via privy:success) dengan token data
terverifikasi. Kode bersifat sekali-pakai & kadaluarsa singkat (±5 menit); tak berguna tanpa API key.
Body
| Field | Type | NotesKeterangan |
|---|---|---|
code required | string | The code from privy:success.exchange_codeKode dari privy:success.exchange_code |
ResponseRespons 200
{ "session_id":"…", "event":"identity.verified", "token":"eyJ… (JWT EdDSA)" }
Public keys to verify the token (RFC 7517). Algorithm EdDSA, curve Ed25519.Kunci publik untuk memverifikasi token (RFC 7517). Algoritma EdDSA, kurva Ed25519.
{ "keys":[{ "kty":"OKP", "crv":"Ed25519", "alg":"EdDSA", "use":"sig", "kid":"…", "x":"…" }]}
Service health check. Returns 200 when healthy.Health check layanan. Mengembalikan 200 bila sehat.
postMessage eventsEvent postMessage
The widget posts events to the parent (iframe) prefixed with privy:. Always validate
event.origin against the widget origin.
Widget mengirim event ke parent (iframe) dengan prefix privy:. Selalu validasi
event.origin terhadap origin widget.
| Event | Payload | WhenKapan |
|---|---|---|
privy:ready | { session_id, state } | Widget finished bootstrappingWidget selesai bootstrap |
privy:success | { session_id, exchange_code, token?, sections? } | Verified & agreedVerifikasi sukses & disetujui |
privy:cancel | { reason } | User cancelled / declinedUser membatalkan / menolak |
privy:error | { code } | Blocked / expired / embed not allowedTerblokir / kadaluarsa / embed ditolak |
{ type } without payload.
Perlindungan payload: data & token hanya dikirim ke embedder yang terverifikasi
(origin ada di allowlist merchant). Untuk host tak dikenal, widget hanya mengirim { type } tanpa payload.
The sections fieldField sections — optional previewpratinjau opsional
The data the user agreed to share, grouped by section (only populated fields). For display only — the authoritative data is always fetched by the backend via exchange → JWT: Data yang disetujui pengguna, dikelompokkan per bagian (hanya field berisi nilai). Untuk menampilkan ke pengguna saja — data resmi tetap diambil backend lewat exchange → JWT:
"sections": [
{ "section":"my_contact_data", "label":"My Contact Data", "fields":[
{ "key":"email", "label":"Email", "value":"user@mail.com", "is_verified":true }
]}
]
Native integration (iOS / Android)Integrasi native (iOS / Android)
Load the session_url directly in a WebView. The same events are delivered via a native bridge:Muat session_url langsung di WebView. Event yang sama dikirim lewat bridge native:
| iOS · WKWebView | Android · WebView |
|---|---|
The widget calls a ReactNativeWebView.postMessage(json) shim. Provide a handler that receives the JSON event.Widget memanggil shim ReactNativeWebView.postMessage(json). Sediakan handler yang menerima JSON event. |
Add a JS interface named PrivyAndroid; the widget calls PrivyAndroid.postMessage(json). Enable WebView camera permission.Tambahkan JS interface bernama PrivyAndroid; widget memanggil PrivyAndroid.postMessage(json). Aktifkan izin kamera WebView. |
// JSON shape received by the native bridgebentuk JSON yang diterima bridge native
{ "type":"privy:success", "payload":{ "session_id":"…", "exchange_code":"…" } }
Data deliveryPengiriman data
Verified data is always delivered as a Privy-signed JWT — regardless of the identity source — through two complementary channels: Data terverifikasi selalu diantar sebagai JWT yang ditandatangani Privy — apapun sumber identitasnya — lewat dua jalur yang saling melengkapi:
- Exchange code → JWT — the frontend receives a single-use
exchange_codeviaprivy:success; the merchant backend swaps it at/v1/sessions/exchangefor a JWT with the data. Avoids the token travelling through the front channel. The code & verified data are held briefly in a fast store (in-memory / Redis) with a short TTL, never in the database. Exchange code → JWT — frontend menerimaexchange_codesekali-pakai viaprivy:success; backend merchant menukarnya di/v1/sessions/exchangeuntuk memperoleh JWT berisi data. Menghindari token melintas front-channel. Kode & data terverifikasi ditahan singkat di penyimpanan cepat (in-memory / Redis) dengan TTL pendek, bukan di basis data. - Server-to-server callback (source of truth) — Privy POSTs the same JWT to the
merchant
callback_url:{ session_id, event:"identity.verified", token }. Callback server-to-server (sumber kebenaran) — Privy mem-POST JWT yang sama kecallback_urlmerchant:{ session_id, event:"identity.verified", token }.
sections field on privy:success is only an optional front-channel preview to
show the user — the authoritative data is always fetched by the backend via exchange → JWT (or the callback).
Field sections pada privy:success hanya pratinjau front-channel opsional untuk
menampilkan data ke pengguna — data resmi selalu diambil backend lewat exchange → JWT (atau callback).
Verify the JWTVerifikasi JWT
The token is an EdDSA / Ed25519 JWT. Verify its signature on your backend with the JWKS, then read the claims: Token adalah JWT EdDSA / Ed25519. Verifikasi tanda tangannya di backend memakai JWKS, lalu baca klaim:
{
"iss": "https://id.privy.id", // issuer
"sub": "<identity ref>",
"aud": "<merchant_id>",
"session_id": "…",
"merchant": "…",
// shared data, grouped by section (fields determined by Privy)data yang dibagikan, dikelompokkan per section (field ditentukan Privy)
"data": {
"my_personal_identity": { "full_name":"…", "identity_number":"…" },
"my_contact_data": { "email":"…", "phone":"…" }
},
"verification": { "method":"phone_lookup+liveness_face_match" },
"iat": …, "exp": …
}
aud = your merchant id, exp not passed, and iss matches.Selalu cek aud = merchant id kamu, exp belum lewat, dan iss sesuai.Security notesCatatan keamanan
- API key stays in the backend. Sessions are created server-side; never expose
X-API-Keyto the browser.API key hanya di backend. Sesi dibuat server-side; jangan exposeX-API-Keyke browser. - Validate
event.originon the postMessage listener — accept only the widget origin.Validasievent.originpada listener postMessage — hanya terima dari origin widget. - Verify the JWT (signature via JWKS +
aud/exp/iss) before trusting the data.Verifikasi JWT (signature via JWKS +aud/exp/iss) sebelum mempercayai data. - The exchange code is useless without the API key — safe to travel the front channel; still single-use & short-lived.Exchange code tak berguna tanpa API key — aman melintas front-channel; tetap single-use & short-lived.
- The iframe must set
allow="camera"; register the merchant origin in the allowlist.iframe wajiballow="camera"; daftarkan origin merchant di allowlist. - Data/token only reach a verified embedder; an unknown host receives the event signal without payload.Data/token hanya sampai ke embedder terverifikasi; host tak dikenal hanya menerima sinyal event tanpa payload.
Error codesKode error
| HTTP | CodeKode | MeaningArti |
|---|---|---|
| 400 | invalid_input | Bad body/parameterBody/parameter tidak valid |
| 401 | unauthorized | Wrong API key / tokenAPI key / token salah |
| 404 | not_found | Session/merchant/code not foundSesi/merchant/kode tidak ditemukan |
| 409 | invalid_state | Transition not allowedTransisi state tidak diizinkan |
| 410 | session_expired | Session expiredSesi kadaluarsa |
| 423 | session_blocked | 3 liveness failuresGagal liveness 3× |
| 429 | rate_limited | Too many requestsTerlalu banyak permintaan |
Error shapeFormat error: { "error": { "code": "…", "message": "…" } }
Changelog
- Initial public release of the Privy Digital ID integration API.Rilis publik pertama API integrasi Privy Digital ID.
- Session flow:
POST /v1/sessions→ embed →privy:success→POST /v1/sessions/exchange.Alur sesi:POST /v1/sessions→ embed →privy:success→POST /v1/sessions/exchange. - Data-sharing consent moved to after the face match; consent screen rendered from the shared-data sections.Consent berbagi data pindah ke setelah pencocokan wajah; layar consent dirender dari section data yang dibagikan.
- Unified delivery: verified data as a JWT via exchange code + callback; exchange held in a fast store (in-memory / Redis).Delivery terpadu: data terverifikasi sebagai JWT via exchange code + callback; exchange ditahan di penyimpanan cepat (in-memory / Redis).
- Optional
sectionspreview onprivy:successfor verified embedders.Pratinjausectionsopsional padaprivy:successuntuk embedder terverifikasi.