API v1 · Digital Identity Verification

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.

Base URL (staging): https://api-digitalid.dcidev.id Auth: X-API-Key Token: JWT EdDSA (Ed25519)

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:

  1. Creates a session from its backend (with an API key) → receives a session_url. Membuat sesi dari backend (dengan API key) → menerima session_url.
  2. Embeds the session_url in an iframe / WebView. Meng-embed session_url di iframe / WebView.
  3. 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.
Integration model: custom session + postMessage (not full OIDC). The session is guarded by a server-side state machine, so steps can't be skipped and reloading the iframe is always safe. Model integrasi: custom session + postMessage (bukan OIDC penuh). Sesi dijaga state machine server-side sehingga langkah tidak bisa dilewati, dan reload iframe selalu aman.

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.

StepLangkahWhat Privy doesYang dilakukan Privy
1 · PhoneNomor HPThe user enters a phone number; Privy matches it against a registered identity.Pengguna memasukkan nomor; Privy mencocokkannya dengan identitas terdaftar.
2 · LivenessFace-liveness detection via camera (anti-spoof, gestures, light reflection).Deteksi keaslian wajah lewat kamera (anti-spoof, gerakan, refleksi cahaya).
3 · Face matchPencocokan wajahThe live selfie is matched against the face on the verified Privy identity.Selfie hidup dicocokkan dengan wajah pada identitas terverifikasi Privy.
4 · ConsentPersetujuanThe user reviews the data to share, then agrees or declines.Pengguna meninjau data yang akan dibagikan lalu menyetujui / menolak.
5 · ResultHasilPrivy 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.

Merchant BE Host Privy Widget Privy API 4 · phone · liveness · face match · consent 1 · POST /v1/sessions → session_url 2 · session_url 3 · embed (iframe / WebView) 5 · privy:success { exchange_code } 6 · exchange_code 7 · POST /v1/sessions/exchange → token 8 · callback (JWT) · source of truth

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
  }
});
Source of truth = the server-to-server callback. postMessage is for UX; business decisions must be validated on the backend (verify the JWT / redeem the exchange code). Sumber kebenaran = callback server-to-server. postMessage untuk UX; keputusan bisnis harus divalidasi backend (verifikasi JWT / tukar exchange code).

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):

created identity_found verified completed
StateScreenLayarMeaningArti
createdPhone inputInput nomor HPSession createdSesi dibuat
identity_foundLiveness (camera)Liveness (kamera)Phone matched → liveness + face match run right awayNomor cocok → liveness + face match langsung berjalan
verifiedConsent screenLayar consentFace matched → the user reviews & agrees/declinesWajah cocok → user meninjau & menyetujui/menolak
completedSuccessSuksesAgreed → privy:successDisetujui → privy:success
identity_not_foundPhone (error)Input nomor (error)Number not registered → retryNomor tidak terdaftar → coba lagi
consent_deniedCancelledDibatalkanUser declined → privy:cancelUser menolak → privy:cancel
blockedBlockedTerblokir3 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.

CredentialKredensialHeaderUsed forDipakai untuk
Merchant API KeyX-API-KeyCreate session & exchange code — from the merchant backend onlyBuat sesi & tukar exchange code — dari backend merchant saja
Never put 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

POST/v1/sessionsX-API-Key

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.

The requested data fields are determined by Privy per the merchant's configuration at registration — not a per-session parameter. Field data yang diminta ditentukan Privy sesuai konfigurasi merchant saat registrasi — bukan parameter per-sesi.

ResponseRespons 201

{ "session_id":"…", "session_url":"https://digitalid.dcidev.id/?t=…", "expires_at":"…Z" }
POST/v1/sessions/exchangeX-API-Key

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

FieldTypeNotesKeterangan
code requiredstringThe code from privy:success.exchange_codeKode dari privy:success.exchange_code

ResponseRespons 200

{ "session_id":"…", "event":"identity.verified", "token":"eyJ… (JWT EdDSA)" }
Verify the token signature on your backend using the JWKS before trusting it.Verifikasi tanda tangan token di backend memakai JWKS sebelum dipercaya.
GET/.well-known/jwks.jsonpublic

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":"…" }]}
GET/healthzpublic

Service health check. Returns 200 when healthy.Health check layanan. Mengembalikan 200 bila sehat.

Endpoints inside the widget (phone input, liveness, consent) are handled entirely by Privy and need not be called by the merchant. Endpoint di dalam widget (input nomor, liveness, consent) ditangani sepenuhnya oleh Privy dan tidak perlu dipanggil merchant.

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.

EventPayloadWhenKapan
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
Payload protection: data & token are sent only to a verified embedder (origin in the merchant allowlist). For an unknown host, the widget sends only { 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 · WKWebViewAndroid · 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 JWTregardless of the identity source — through two complementary channels: Data terverifikasi selalu diantar sebagai JWT yang ditandatangani Privyapapun sumber identitasnya — lewat dua jalur yang saling melengkapi:

  1. Exchange code → JWT — the frontend receives a single-use exchange_code via privy:success; the merchant backend swaps it at /v1/sessions/exchange for 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 menerima exchange_code sekali-pakai via privy:success; backend merchant menukarnya di /v1/sessions/exchange untuk 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.
  2. 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 ke callback_url merchant: { session_id, event:"identity.verified", token }.
The 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": 
}
Always check 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-Key to the browser.API key hanya di backend. Sesi dibuat server-side; jangan expose X-API-Key ke browser.
  • Validate event.origin on the postMessage listener — accept only the widget origin.Validasi event.origin pada 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 wajib allow="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

HTTPCodeKodeMeaningArti
400invalid_inputBad body/parameterBody/parameter tidak valid
401unauthorizedWrong API key / tokenAPI key / token salah
404not_foundSession/merchant/code not foundSesi/merchant/kode tidak ditemukan
409invalid_stateTransition not allowedTransisi state tidak diizinkan
410session_expiredSession expiredSesi kadaluarsa
423session_blocked3 liveness failuresGagal liveness 3×
429rate_limitedToo many requestsTerlalu banyak permintaan

Error shapeFormat error: { "error": { "code": "…", "message": "…" } }

Changelog

v1.0Jul 2026
  • 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:successPOST /v1/sessions/exchange.Alur sesi: POST /v1/sessions → embed → privy:successPOST /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 sections preview on privy:success for verified embedders.Pratinjau sections opsional pada privy:success untuk embedder terverifikasi.
Future versions and their changes will be listed here, selectable from the version dropdown at the top. Versi berikutnya beserta perubahannya akan dicantumkan di sini, dapat dipilih dari dropdown versi di atas.