Document data capture
Read the fields of an identity document from a photo, to prefill a form that a person then confirms. The photo is encrypted on the device; your backend fetches the fields once, with its secret key.
A DocumentExtraction holds fields and a per-field confidence. It says nothing about whether the document is genuine: any photo, screen or edited image can produce one. It has no identity key, no assurance level and no decision. Never grant anything on it — treat every value as unconfirmed input for a person to check, and use the verification flow when you need a verified identity.
Document data capture is in early access until it is generally available; the date will be announced on the changelog. The examples on this page show "environment": "LIVE" to illustrate the contract; during early access, request access by writing to sales@emnetid.com before you plan a LIVE integration.
Who does what
The work is split between your backend and the user’s device, the same way as verification.
| Step | Where | Key | Operation |
|---|---|---|---|
| 1. Start a capture | Your backend (or a native app) | sk_ (or pk_ in a native app) | startDocumentExtraction — returns the handle, a one-time evidence token, the parts to photograph and the accepted upload-key thumbprints. |
| 2. Encrypt and upload each photo | The device (browser or mobile app) | pk_ | uploadExtractionEvidence, through the document-capture package, which encrypts the image on the device before it is sent. |
| 3. Fetch the fields | Your backend only | sk_ | fetchExtractionFields — single-shot: on a final answer the token is consumed and the evidence destroyed. |
A publishable key never receives a field, and a secret key never belongs on a device or in a browser. Start may also be called with a publishable key from a native app; it is not available to browsers (there is no cross-origin access to it), so a web app always starts the capture from its backend and hands the Start response to the page.
Document types
Declare the type when you start. It is a routing hint, never evidence: the issuing country, for example, is read from the document itself. The type codes are frozen; new types are only ever added. Each type must be enabled for your account.
| documentType | Required parts | Document |
|---|---|---|
FAYDA_DIGITAL_ID | FRONT, BACK | The Ethiopian Fayda digital ID card |
PASSPORT | DATA_PAGE | Any ICAO 9303 passport data page |
ID_CARD_MRZ | FRONT, BACK | An identity card with an ICAO 9303 machine-readable zone (TD1/TD2) |
US_DRIVER_LICENSE | FRONT, BACK | A US driver licence or state ID with an AAMVA PDF417 barcode on the back |
ETHIOPIAN_KEBELE_ID | FRONT (BACK accepted, optional) | An Ethiopian kebele resident ID card |
The flow
1. Start
POST /v1/document/extractions with {"documentType": "PASSPORT"} returns:
{
"extractionHandle": "ex_0f3c9a1b7d2e4f60a8b9c0d1e2f3a4b5",
"evidenceToken": "…",
"evidenceTokenExpiresAt": "2026-10-10T09:15:00Z",
"requiredEvidence": ["DATA_PAGE"],
"acceptedEvidence": ["DATA_PAGE"],
"uploadKeyThumbprints": ["…"],
"environment": "LIVE"
}The evidenceToken is returned once and never again. It is a bearer secret for this one capture: hand it only to the device that uploads and keep it for the fields fetch. Never log it and never put it in a URL. The capture expires 15 minutes after Start. requiredEvidence lists the parts that must be uploaded before the fields can be fetched; acceptedEvidence lists every part the upload admits — the required parts plus any optional one. A part outside the accepted set is refused (422); a fetch before every required part is uploaded is 409 EVIDENCE_INCOMPLETE.
2. Upload each part, encrypted on the device
The device photographs each part in requiredEvidence, encrypts it to Emnet’s upload key and uploads the ciphertext with your publishable key. Upload each part once; a retake is a new capture. A kebele card’s BACK is optional: it is in acceptedEvidence but not in requiredEvidence, so it may be uploaded and is not needed to fetch the fields. If an upload answers 503 the part was not stored — retry it. Nothing is extracted at upload: the part is only checked and stored, encrypted, until your backend fetches the fields. Use the document-capture package for this step; the envelope it builds is specified below.
3. Fetch the fields — once
When every part is uploaded, your backend calls POST /v1/document/extractions/{handle}/fields with the evidence token and a secret key. On a final answer (200, or 422 EVIDENCE_UNREADABLE) the token is consumed and the evidence and its keys are destroyed in the same call. A second fetch after a final answer answers 404: start a new capture (a retake). Emnet never stores extracted fields.
503 EXTRACTION_UNAVAILABLE is transient and retryable with the same token: the token is not consumed and the evidence is kept — this includes a response cut short by the server’s time budget for the call. Each such attempt counts toward a bounded number of extraction attempts; once the bound is spent the call answers 404 and a new capture is needed. Retry with back-off before the token expires. A 503 is an answer; a 504 or no answer is not.
A 504 or a dropped connection on the fields 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 capture, or ask the device for a retake.
{
"extractionHandle": "ex_0f3c9a1b7d2e4f60a8b9c0d1e2f3a4b5",
"documentType": "PASSPORT",
"extractionMethod": "MRZ",
"environment": "LIVE",
"fields": {
"surname": { "value": "TESFAYE", "confidence": 0.95 },
"given_names": { "value": "SELAM", "confidence": 0.95 },
"birthdate": { "value": "1994-03-12", "confidence": 0.95 },
"document_expiry": { "value": "2031-05-30", "confidence": 0.95 },
"place_of_birth": { "value": "ADDIS ABABA", "confidence": 0.6 }
},
"missing": ["document_issue_date"]
}Fields
Which fields you receive is set per document type by your account’s field policy (default: none). fields holds only the fields that were read and are allowed; missing lists the allowed fields that could not be read. No other key ever appears.
| documentType | Fields it can yield |
|---|---|
FAYDA_DIGITAL_ID | name, name_am, birthdate, birthdate_ethiopian, nationality, nationality_self_declared, document_issuing_country, document_number_last4, document_issue_date |
PASSPORT | name, given_names, surname, birthdate, sex, nationality, document_issuing_country, document_number, document_number_last4, document_expiry, document_issue_date, place_of_birth |
ID_CARD_MRZ | name, given_names, surname, birthdate, sex, nationality, document_issuing_country, document_number, document_number_last4, document_expiry, document_issue_date, place_of_birth |
US_DRIVER_LICENSE | name, given_names, surname, birthdate, document_issuing_country, document_number, document_number_last4, document_expiry, address_line1, address_line2, city, state, postal_code |
ETHIOPIAN_KEBELE_ID | name, name_am, birthdate, birthdate_ethiopian, nationality, nationality_self_declared, document_issuing_country, document_number, document_number_last4, document_expiry, document_issue_date |
| Field | Format |
|---|---|
birthdate | ISO-8601 YYYY-MM-DD (Gregorian). Also document_expiry and document_issue_date. |
birthdate_ethiopian | YYYY-MM-DD in the Ethiopian calendar, as printed. |
name_am | The name in Ethiopic script. |
sex | M or F. Passports and MRZ identity cards only. |
nationality | ISO 3166-1 alpha-3. |
nationality_self_declared | The nationality word as printed on an Ethiopian card — not a code. |
document_issuing_country | ISO 3166-1 alpha-2, read from the document, never from the declared type. |
document_number_last4 | The last four characters of the document number. |
document_expiry | Only when printed or encoded on the document — never inferred. |
address_line1 … postal_code | US driver licences only. |
- Confidence. About 0.95 for a structured read (a barcode, or a machine-readable zone whose check digits passed without correction); about 0.6 for text read from the printed face. Flag any field below 0.80 for review.
repairedis present, andtrue, only when the value passed its check digit after a single-character correction of a likely misread. It is evidence about extraction, never a verdict.- The document number. For
FAYDA_DIGITAL_IDonlydocument_number_last4is ever returned. For the other types the fulldocument_numberis returned only when your field policy lists it. extractionMethod—OCR,MRZ,BARCODEorQR— says how the fields were read, never whether the document is authentic.environmentsays which key environment produced the result; it does not make it a verification.
The evidence envelope
The uploaded file is never an image. It is a JWE (RFC 7516) in compact serialization, content type application/jose, whose plaintext is the JPEG or PNG image. The document-capture package builds it for you; this is the binding specification.
algexactlyECDH-ES+A256KWon curve P-256,encexactlyA256GCM, with a fresh ephemeral key pair for every part (epk: exactlykty,crv,x,y).- A fresh content-encryption key and a fresh 96-bit IV for every part, from a cryptographically secure random generator — never reused across parts or captures.
kidis the published name of the Emnet upload key the part is encrypted to; the RFC 7638 thumbprint is the trust anchor you pin. The only other header parameters allowed aretypandcty; a part carryingzip,crit,jku,x5u,jwk,x5c,x5t,x5t#S256,apu,apvor any other parameter, or any otheralgorenc, is refused.- Plaintext: JPEG or PNG only (detected by content, not by name), 200–6000 px per side, at most 25 megapixels and 8 MiB (8,388,608 bytes). HEIC/HEIF is refused — export JPEG on the device. The compact serialization is at most 11,534,336 bytes.
The Emnet upload public keys you may encrypt to are pinned in your app build (the document-capture package carries them) — never fetched at run time from an Emnet URL. The Start response’s uploadKeyThumbprints (RFC 7638) is the set Emnet accepts now, two during a key rotation, and is never a source of keys. Encrypt to a pinned key whose thumbprint is in that set; if none is, do not upload — update the app. A key rotation is therefore an app release.
The pin is only as trustworthy as the JavaScript that carries it. Deliver your web app from your own origin — never through Emnet’s edge — and bundle the package into your build rather than loading it from a CDN. Cross-origin uploads are accepted only from origins registered for your account in the key’s environment; the only request header a browser may add is Authorization, and no cookies are used.
Request rules
- No query string on any document data-capture operation: a request with one is refused (
400, or404) — the token travels only in the body. Malformed paths are refused (400). - A
Content-Lengthheader is required and chunked bodies are refused (411). Bodies are bounded (413). These refusals may arrive with an empty body. - Every ownership mismatch — unknown handle, wrong or expired token, another account’s or another environment’s capture, a capture already fetched, an unregistered origin — is the same
404 NOT_FOUND. It never reveals which.
Errors
{
"success": false,
"error": { "code": "EVIDENCE_UNREADABLE", "message": "evidence could not be read — retake", "retakeHint": "GLARE" }
}| Status | Code | Meaning | What to do |
|---|---|---|---|
400 | INVALID_REQUEST | Malformed body, unknown documentType, malformed multipart, or a query string | Fix the request. |
401 | — | Missing or invalid API key | Check the key. |
403 | FORBIDDEN | A publishable key on the fields fetch; or capture, this type or a field policy is not enabled for your account | Use a secret key on your backend; ask us to enable the type. |
404 | NOT_FOUND | Any ownership mismatch (see above) | Start a new capture. |
409 | EVIDENCE_ALREADY_RECEIVED | The part was already uploaded | A retake is a new capture. |
409 | EVIDENCE_INCOMPLETE | Fields fetched before every part was uploaded | Upload the missing parts; the token is not consumed. |
411 | — | No Content-Length, or a chunked body | Send the body with a declared length. |
413 | PAYLOAD_TOO_LARGE | The body exceeds the ceiling | Send a smaller photo. |
422 | EVIDENCE_UNREADABLE | At upload: the part broke an envelope, size or format rule (no hint). At the fetch: the photos could not be read; retakeHint says why | Retake — start a new capture. |
429 | — | Rate limited | Back off and retry. |
503 | CAPTURE_DISABLED / EXTRACTION_UNAVAILABLE | Capture is not available, or the service is temporarily busy (on an upload the part was not stored; on the fields call the evidence is kept and attempts are counted); the token is not consumed | Retry with back-off before the token expires; on the fields call, once the attempt bound is spent the answer is 404. |
retakeHint is one of BLUR, DARK, GLARE, NO_TEXT, BARCODE_UNREADABLE or UNSUPPORTED_DOCUMENT. It describes image quality only, never authenticity — use it to ask for a better photo.
Next
- Document-capture package — install and use it in a browser or a React Native app.
- Java SDK —
startDocumentExtractionandfetchExtractionFieldson your backend. - API reference — the three operations.