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.
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://api-digitalid.dcidev.id/v1/sessions \
-H "X-API-Key: <MERCHANT_API_KEY>"
{
"session_id": "9f1c…",
"session_url": "https://digitalid.dcidev.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://digitalid.dcidev.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://digitalid.dcidev.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.