Skip to main content

Document scanning (OCR)

POST /v1/ocr/scan reads a Bhutanese identity document and returns the extracted fields together with a verdict on whether the scan can be relied on. The verdict is the part integrators most often get wrong: a scan that reads every field can still be ineligible, and an ineligible result carries no fields at all.

Supported documents

document_typeDocumentSides requiredFields returned when eligible
cidCitizenship Identity Cardfront and backfull_name, cid_number, date_of_birth, sex, date_of_issue, date_of_expiry
srcSpecial Resident Cardfront and backfull_name, srp_number, date_of_birth, sex, date_of_issue, date_of_expiry
passportBhutanese passport, data pagefrontfull_name, given_names, surname, document_number, personal_number, date_of_birth, date_of_expiry, nationality, issuing_country, sex
work_permitWork permit, standard typefrontfull_name, work_permit_number, date_of_birth, nationality, job_category, employer, location, date_of_issue, date_of_expiry

The document type is detected from the image. The optional type field is a hint and must be one of the four values above; anything else is a validation_error.

Request

multipart/form-data with these parts:

PartRequiredNotes
imageyesFront of the document. JPEG, PNG, HEIC, WebP, TIFF or BMP, or a PDF, up to 10 MB. Phone photos can be sent as they come off the camera; HEIC needs no conversion.
back_imagefor cid and srcBack of the same card. Same formats and limit.
typenoHint: cid, src, passport or work_permit.
customer_refnoYour own identifier for the end user; echoed in the result and webhook.

An upload over the size limit is rejected with a 400 validation_error whose message starts with upload rejected.

Image requirements

The platform refuses captures it cannot trust rather than guessing. Each rule below maps to a rejection code you will see in the result.

  • Colour photo of the physical document. A grayscale scan or a photocopy saved as an image is refused (monochrome_image). The check measures colour saturation, so it does not depend on the file format.
  • At least 550 px on the long side and 350 px on the short side (insufficient_image_quality). That is the floor for the densest documents; a CID needs closer to 850 px on the long side before every field reads, so send phone photos at full resolution rather than thumbnails.
  • Sharp, well lit, upright, card filling most of the frame. Blur is forgiven when every required field still reads; a card photographed sideways is not (orientation_unresolved).
  • Both sides of a two-sided card must be the same card. The small ghost portrait printed on the front and back of a CID or SRC is matched server side. Sides from two different cards are rejected (side_mismatch); a pair whose ghosts cannot be found or compared is rejected as side_binding_unavailable, and the fix is to retake both sides with the small portrait clearly visible.
  • The card does not have to fill the frame. For a CID the card is located inside the photo and the matching runs on the card itself, so a phone photo of a card on a desk works as long as the resolution rule above holds for the whole image.

Reading the result

{
"ocr_id": "ocr_01a0799179d576da…",
"object": "ocr.result",
"document_type": "cid",
"document_type_confidence": 0.98,
"ocr_eligible": false,
"decision_version": "ocr-eligibility-v1",
"eligibility_reason": "expired_document",
"eligibility_message": "The document has expired. A renewed document is required.",
"gate_rejections": [
{
"code": "expired_document",
"label": "Expired document",
"message": "The expiry date is before the server evaluation date."
}
],
"fields": {},
"raw_text": { "front": "KINGDOM OF BHUTAN\n…", "back": "Date of Issue: 04/03/2016\n…" },
"portrait": null,
"customer_ref": "user-8812",
"stored": true,
"expires_at": "2026-12-06T01:52:52.181Z",
"request_id": "01a07991-79d5-76da-a6bc-1020aa49ba28",
"created_at": "2026-09-07T01:52:52.181Z",
"processing_time_ms": 3295
}
  • ocr_eligible is the only field to build on. It is true only when every check passed. When it is false, fields is an empty object and portrait is null, whatever the model read.
  • eligibility_reason names the first failing check and eligibility_message is a sentence you can show the end user unchanged.
  • gate_rejections lists every failing check, not just the first, so a scan that is both expired and unreadable on one line reports both.
  • raw_text is the recognised text per side, for debugging and review. Do not parse it yourself: it is exactly what the eligibility gate already judged insufficient.
  • portrait is the cropped photo from the document, as a signed URL when storage is on and inline base64 otherwise.

Eligibility reasons

eligibility_reasonWhat to tell the user
eligibleAll required checks passed.
unsupported_document_typeThe document could not be identified as a supported Bhutanese identity document.
unsupported_document_subtypeThis document type is not accepted for identity verification.
non_bhutanese_passportOnly Bhutanese passports are accepted.
missing_required_sideUpload both the front and the back.
insufficient_image_qualityToo blurry or too small; retake in good light, filling the frame with the card.
monochrome_imageThe image is black and white; take a colour photo of the physical document.
orientation_unresolvedPhotograph the card upright and square to the camera.
missing_required_fieldOne or more required fields could not be read.
invalid_field_formatA field was read but its value is not in the expected format.
invalid_date, future_date, invalid_chronologyA date could not be read, is in the future, or the birth, issue and expiry dates are inconsistent.
expired_documentThe document has expired; a renewed document is required.
invalid_mrzThe passport's machine-readable zone failed its checksums.
side_mismatchThe front and back are from different cards.
side_binding_unavailableThe photos on the front and back could not be matched; retake both sides.
field_conflictThe same field was read with conflicting values.
suspected_tamperThe document shows signs of tampering.
unreadable_documentNo readable text was found.
insufficient_evidenceNot enough confidence to accept; retake showing the whole card.

Gate rejection codes

Codes are stable strings. Field-scoped codes carry the field name after a colon, for example missing_required_field:cid_number.

CodeMeaning
unreadable_documentNo readable OCR text was produced.
unsupported_document_type / unsupported_document_subtypeNot a supported document, or a permit subtype that is not accepted.
missing_required_side:front / :backThe document type requires that side.
orientation_unresolvedThe orientation could not be resolved with confidence.
insufficient_image_quality:front / :backThat side failed the resolution or blur gate.
monochrome_image:front / :backThat side has no colour: a grayscale scan or photocopy.
side_binding_unavailableThe front and back could not be positively matched.
side_mismatchThe front and back are different cards.
missing_required_field:<field>No value was read for a required field.
invalid_field_format:<field>The value is not in the field's required format.
missing_field_evidence:<field>The value was not backed by OCR geometry on the image.
field_conflict:<field> / field_conflicts_unresolvedConflicting values for a field.
invalid_date:<date field> / future_date:<date field> / invalid_chronologyDate problems.
expired_documentThe expiry date is before the evaluation date.
missing_document_evidenceThe issuer and document title were not found on the front.
non_bhutanese_passport / invalid_mrz / missing_visual_mrz_agreement / missing_place_of_birth_polygonPassport-specific checks.
suspected_tamperAn anti-tamper signal vetoed eligibility.
eligibility_disabledThe service is running in fail-closed mode; every scan is ineligible.

Storage and retention

What is kept follows your tenant's storage policy (Dashboard, Settings, Storage policy):

  • Results (store_results): the fields and diagnostics, for result_ttl_days. stored and expires_at in the response say whether and until when.
  • Source images (store_source_images): the uploaded photos and, for CIDs, the automatic card crops used for side matching, for source_image_ttl_days. These appear on the scan's page in the dashboard for review.
  • The portrait is kept with the result and served through short-lived signed URLs.

Test-mode results are always short-lived regardless of policy; see Test mode vs live mode.

Webhook

A completed scan publishes ocr.completed with the same body as the response. See Webhooks.