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.

Data capture, not verification

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.

Early access

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.

StepWhereKeyOperation
1. Start a captureYour 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 photoThe 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 fieldsYour backend onlysk_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.

documentTypeRequired partsDocument
FAYDA_DIGITAL_IDFRONT, BACKThe Ethiopian Fayda digital ID card
PASSPORTDATA_PAGEAny ICAO 9303 passport data page
ID_CARD_MRZFRONT, BACKAn identity card with an ICAO 9303 machine-readable zone (TD1/TD2)
US_DRIVER_LICENSEFRONT, BACKA US driver licence or state ID with an AAMVA PDF417 barcode on the back
ETHIOPIAN_KEBELE_IDFRONT (BACK accepted, optional)An Ethiopian kebele resident ID card

The flow

1. Start

POST /v1/document/extractions with {"documentType": "PASSPORT"} returns:

json
{
  "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.

No answer is not a failure

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.

json
{
  "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.

documentTypeFields it can yield
FAYDA_DIGITAL_IDname, name_am, birthdate, birthdate_ethiopian, nationality, nationality_self_declared, document_issuing_country, document_number_last4, document_issue_date
PASSPORTname, given_names, surname, birthdate, sex, nationality, document_issuing_country, document_number, document_number_last4, document_expiry, document_issue_date, place_of_birth
ID_CARD_MRZname, given_names, surname, birthdate, sex, nationality, document_issuing_country, document_number, document_number_last4, document_expiry, document_issue_date, place_of_birth
US_DRIVER_LICENSEname, given_names, surname, birthdate, document_issuing_country, document_number, document_number_last4, document_expiry, address_line1, address_line2, city, state, postal_code
ETHIOPIAN_KEBELE_IDname, name_am, birthdate, birthdate_ethiopian, nationality, nationality_self_declared, document_issuing_country, document_number, document_number_last4, document_expiry, document_issue_date
FieldFormat
birthdateISO-8601 YYYY-MM-DD (Gregorian). Also document_expiry and document_issue_date.
birthdate_ethiopianYYYY-MM-DD in the Ethiopian calendar, as printed.
name_amThe name in Ethiopic script.
sexM or F. Passports and MRZ identity cards only.
nationalityISO 3166-1 alpha-3.
nationality_self_declaredThe nationality word as printed on an Ethiopian card — not a code.
document_issuing_countryISO 3166-1 alpha-2, read from the document, never from the declared type.
document_number_last4The last four characters of the document number.
document_expiryOnly when printed or encoded on the document — never inferred.
address_line1 … postal_codeUS 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.
  • repaired is present, and true, 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_ID only document_number_last4 is ever returned. For the other types the full document_number is returned only when your field policy lists it.
  • extractionMethod — OCR, MRZ, BARCODE or QR — says how the fields were read, never whether the document is authentic. environment says 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.

  • alg exactly ECDH-ES+A256KW on curve P-256, enc exactly A256GCM, with a fresh ephemeral key pair for every part (epk: exactly kty, 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.
  • kid is 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 are typ and cty; a part carrying zip, crit, jku, x5u, jwk, x5c, x5t, x5t#S256, apu, apv or any other parameter, or any other alg or enc, 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.
Key pinning

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.

Web apps: serve your own bundle

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, or 404) — the token travels only in the body. Malformed paths are refused (400).
  • A Content-Length header 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

json
{
  "success": false,
  "error": { "code": "EVIDENCE_UNREADABLE", "message": "evidence could not be read — retake", "retakeHint": "GLARE" }
}
StatusCodeMeaningWhat to do
400INVALID_REQUESTMalformed body, unknown documentType, malformed multipart, or a query stringFix the request.
401—Missing or invalid API keyCheck the key.
403FORBIDDENA publishable key on the fields fetch; or capture, this type or a field policy is not enabled for your accountUse a secret key on your backend; ask us to enable the type.
404NOT_FOUNDAny ownership mismatch (see above)Start a new capture.
409EVIDENCE_ALREADY_RECEIVEDThe part was already uploadedA retake is a new capture.
409EVIDENCE_INCOMPLETEFields fetched before every part was uploadedUpload the missing parts; the token is not consumed.
411—No Content-Length, or a chunked bodySend the body with a declared length.
413PAYLOAD_TOO_LARGEThe body exceeds the ceilingSend a smaller photo.
422EVIDENCE_UNREADABLEAt upload: the part broke an envelope, size or format rule (no hint). At the fetch: the photos could not be read; retakeHint says whyRetake — start a new capture.
429—Rate limitedBack off and retry.
503CAPTURE_DISABLED / EXTRACTION_UNAVAILABLECapture 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 consumedRetry 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