Native eKYC capture and the verified photo
Your app captures the document, the selfie and the liveness frames with its own camera code and sends them straight to Authpermit. No web view, no hosted page. Your API key never reaches the phone: the phone holds a client secret for one verification session, and your backend reads the decision.
1. Your backend starts the session
curl -X POST https://authpermit.com/api/v1/verification-sessions \
-H "Authorization: Bearer ap_live_..." -H "Idempotency-Key: ..." \
-H "Content-Type: application/json" \
-d '{"person_id":"per_...","checks":["document","face","liveness"],"required_assurance":3}'
Retrying with the same Idempotency-Key and body returns the same session and, for 15 minutes, the same working client_secret (kept encrypted for that window only). After that a replay returns the session with client_secret: null and client_secret_status: "unavailable_on_replay": start a new session with a new key. A replay never returns a secret that does not work.
The response carries client_secret (vcs_…) and capture_base_url. Give them only to this person's own app. The secret works for 24 hours, for this session, on /capture/* only. The liveness challenge is chosen by Authpermit; any metadata.challenge you send is ignored.
2. The app captures (Authorization: Bearer vcs_…)
| Call | What it does |
|---|---|
GET /capture/session | Steps, the consent text and its version, the liveness challenge (type turn/blink/smile, direction for turn, nonce, 4–8 frames), limits (JPEG/PNG, 10 MB). |
POST /capture/consent | {"accepted":true,"text_version":"…","locale":"en"} → 204. Every upload before it is refused with 409 consent_required. |
POST /capture/documents | multipart file + kind + side (or JSON data). 422 retake with issues when the photo is blurry, dark, glared or unreadable; nothing is stored. |
POST /capture/selfie | 422 retake (no_face, multiple_faces) unless there is exactly one face. |
POST /capture/liveness | multipart frames[] (or JSON frames) recorded while the person follows the challenge, with challenge_nonce. A wrong nonce is 422 challenge_mismatch. |
POST /capture/submit | 202. 422 capture_incomplete names what is missing. |
GET /capture/status | collecting, processing, done, review, cancelled or expired. Never the outcome. |
Send upright pixels (EXIF orientation is not applied). After submit the secret can only read the status.
3. Your backend reads the decision
Subscribe to verification.* and person.* webhooks, check X-Authpermit-Signature, deduplicate on X-Authpermit-Event-ID, then call GET /verification-sessions/{id}/result: status, outcome, assurance, per-check results, document type and country, and a name check against the name you linked. A face or liveness result of unavailable is never a pass and caps assurance at 2. The legal name is included only with identity:read:sensitive.
4. The verified photo
When face match and liveness both pass, Authpermit keeps a verified portrait: one face cropped from the selfie, at most 512 px, re-encoded so no EXIF or location survives, encrypted at rest. person.photo_available tells you it exists.
To show it — for example to the driver arriving for pickup:
- The person consents in your app; your backend records it:
POST /people/{id}/consentswith{"purpose":"identity_display","scope":["verified_photo"],"text_version":"…","locale":"en","captured_at":"…","ip":"…","device_id":"…"}(scopeconsent:manage). - At pickup:
POST /people/{id}/photo-grantswith{"viewer_ref":"drv_221","context_ref":"ride_789","ttl_seconds":600,"max_views":5}(scopeidentity:photo:read, never a default). You get a URL,…/api/v1/photos/pt_…, valid for at most 15 minutes and at most 20 views. Hand it to that driver's app only. - The URL serves
image/jpegwithno-store,nosniffand a sandboxed CSP; afterwards it answers 410.
Refusals: 403 consent_required, 403 not_linked, 404 photo_unavailable. Cancel with DELETE /photo-grants/{id}. When the person revokes the consent (POST /consents/{id}/revoke), every outstanding URL stops at once. GET /people/{id}/photo-accesses lists every grant and view, for the person to see.
Older integrations
Backends that upload server-side (POST /verification-sessions/{id}/documents) may keep sending identity_document with id_front/id_back/selfie; they are mapped to the canonical values. A single selfie is not liveness: without frames the liveness check does not pass.
Agents
MCP has authpermit_verification_result and authpermit_person_photo_status (availability and the access log). No tool returns a face or a photo URL, and no tool records consent.