Document verification
Verify a person from a photo of an identity document, for someone who has no registry route. The result is the same claims-only VerifiedIdentity as the registry flow, with an honest assurance level — never a decision.
Document verification is in early access until it is generally available; the date will be announced on the changelog. Request access by writing to sales@emnetid.com.
What it can and cannot prove
A label on the wire says only what was proven. A photo of a document proves nothing about whether the document is genuine — any photo, screen or edited image can produce one — unless the document carries a signature that can be checked. So:
- On LIVE keys, only the Fayda card can be verified, through the signed QR code on its back (
DOCUMENT_SIGNED_QR). In LIVE only this signed tier can complete, and only once it is enabled for your account. - Passports, MRZ identity cards, US driver licences and kebele IDs are never verified on LIVE. For them, use document data capture: fields to prefill a form a person confirms, with no claim of authenticity.
- Any environment Emnet hosts applies the LIVE rule to TEST keys too. A hosted sandbox starts only Fayda-card sessions and completes only a verified signed QR, so what you build against it behaves the way it will in production.
- On a LIVE key, any outcome other than a verified signed QR is
400 VERIFICATION_FAILED— never a weaker tier relabelled as a pass.
Assurance levels
| assuranceLevel | What was proven | LIVE? |
|---|---|---|
DOCUMENT_SIGNED_QR | The Fayda card’s signed QR code was verified. | Yes, once enabled for your account |
DOCUMENT_MRZ_CHECKED | Every machine-readable-zone check digit verified without correction — the document is self-consistent, not authentic. | No — TEST only |
DOCUMENT_BARCODE_PARSED | A licence barcode was decoded. It is unsigned. | No — TEST only |
DOCUMENT_OCR | Printed text was read. | No — TEST only |
The three unsigned tiers prove nothing about authenticity. No environment Emnet hosts returns them; they are in the contract so that your code can name and handle them. assuranceLevel is a string, not a closed list: treat any value you do not recognise as not verified.
Who does what
| Step | Where | Key | Operation |
|---|---|---|---|
| 1. Start a session | Your backend (or a native app) | sk_ (or pk_ in a native app) | startDocumentVerification — returns sessionId, the device’s sessionHandle, a one-time evidence token, the parts to photograph and the accepted upload-key thumbprints. |
| 2. Encrypt and upload each photo | The device | pk_ | uploadVerificationEvidence, through the document-capture package — the same encrypted envelope and key pinning as data capture. |
| 3. Complete and receive the claims | Your backend only | sk_ | completeDocumentVerification — returns the VerifiedIdentity. |
Start is not available to browsers (there is no cross-origin access to it), and completion is never available to a browser or a device: a secret key belongs on your backend only. A web app starts the session on its backend and hands the Start response to the page.
The flow
1. Start
POST /v1/verification/person/document/sessions with {"documentType": "FAYDA_DIGITAL_ID"}. The document type must be enabled for your account.
{
"sessionId": 4711,
"sessionHandle": "ds_0f3c9a1b7d2e4f60a8b9c0d1e2f3a4b5",
"evidenceToken": "…",
"evidenceTokenExpiresAt": "2026-10-10T09:15:00Z",
"requiredEvidence": ["FRONT", "BACK"],
"acceptedEvidence": ["FRONT", "BACK"],
"uploadKeyThumbprints": ["…"],
"environment": "TEST"
}sessionId is your address for the session; it also works with Read a session. sessionHandle is the device’s address. The evidenceToken is returned once: hand it only to the device that uploads and keep it for completion. Never log it and never put it in a URL.
2. Upload each part, encrypted on the device
POST /v1/verification/person/document/sessions/{sessionHandle}/evidence, exactly like a capture upload: the same evidence envelope, the same pinned upload keys, the same browser rules, any part in acceptedEvidence, each part once. Nothing is verified at upload. The document-capture package recognises a verification Start response and uploads to the right place.
3. Complete — on your backend
POST /v1/verification/person/document/sessions/{sessionHandle}/complete with the evidence token and a secret key.
| Answer | Meaning | What to do |
|---|---|---|
200 | Completed; the evidence is destroyed. | Read the VerifiedIdentity (below). |
400 VERIFICATION_FAILED | The document could not be verified and the session is finished. The response never says why. | Do not retry the session. |
422 EVIDENCE_UNREADABLE | A retake, not a failure. The session and its token are kept, but every uploaded part has been destroyed. retakeHint says why (image quality only). | Re-upload EVERY part, then complete again. Retakes are limited; after the last one the session fails. |
409 EVIDENCE_INCOMPLETE | A required part has not been uploaded yet; nothing changes. | Upload it, then complete. |
503 PROVIDER_UNAVAILABLE | Temporarily unavailable. | Retry with back-off before the token expires. |
With the document-capture package, a retake means a new uploader created from the same Start response, which sends every part again.
A 504 or a dropped connection on the completion call does not mean the request failed: the server may already have consumed the one-time evidence token and destroyed the evidence. Do not retry the same call expecting a result — a retry answers 404. Start a new session, or ask the device for a retake.
The result
{
"sessionId": 4711,
"identityKey": "3f9c1a0b7e5d4c2a9b8e7f6d5c4b3a29…",
"identityKeyType": "DOCUMENT_PSEUDONYM",
"claims": {
"name": "…",
"name_am": "…",
"birthdate": "…",
"document_type": "FAYDA_DIGITAL_ID",
"document_issuing_country": "ET",
"document_number_last4": "…"
},
"claimConfidence": { "name": 0.95, "name_am": 0.95, "birthdate": 0.95 },
"assuranceLevel": "DOCUMENT_SIGNED_QR",
"livenessEvidence": "NONE",
"provider": "EMNET_DOCUMENT",
"source": "DOCUMENT_UPLOAD",
"environment": "LIVE"
}identityKeyTypeisDOCUMENT_PSEUDONYMon this lane andFIN_PSEUDONYMon the registry lane. The same person has two different keys on the two lanes: key your records by (identityKeyType,identityKey) and never join across types. A document pseudonym changes when the document is renewed. An older service may not send the field — never assume a type.claimConfidencegives, per released claim read from the document, the extraction confidence in [0, 1]. It is evidence about extraction, never a decision:1.0does not mean “verified”.- New claims on this lane (released only when your account is entitled to them):
document_type,document_issuing_country(ISO 3166-1 alpha-2),document_number_last4,document_issue_date,name_am(Ethiopic script),nationality_self_declared("true"when the card printed a self-declared nationality) andgenderas read from the document. Never released on any lane: the full document number (only its last four characters), the national ID number (individual_id) and the photo (picture). Your code must refuse a result that carries any of the three, whatever their letter case or surrounding spaces. livenessEvidenceis alwaysNONE: the document lane does no liveness check.- The consumer rule still binds: under your production profile, grant anything from a result only if
environmentis exactly"LIVE". The decision is yours; Emnet returns claims, not decisions.
Next
- Document-capture package — the device-side upload, for captures and verification sessions.
- Java SDK —
startDocumentVerificationandcompleteDocumentVerificationon your backend. - Claims & identity keys and the API reference.