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_type | Document | Sides required | Fields returned when eligible |
|---|---|---|---|
cid | Citizenship Identity Card | front and back | full_name, cid_number, date_of_birth, sex, date_of_issue, date_of_expiry |
src | Special Resident Card | front and back | full_name, srp_number, date_of_birth, sex, date_of_issue, date_of_expiry |
passport | Bhutanese passport, data page | front | full_name, given_names, surname, document_number, personal_number, date_of_birth, date_of_expiry, nationality, issuing_country, sex |
work_permit | Work permit, standard type | front | full_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:
| Part | Required | Notes |
|---|---|---|
image | yes | Front 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_image | for cid and src | Back of the same card. Same formats and limit. |
type | no | Hint: cid, src, passport or work_permit. |
customer_ref | no | Your 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 asside_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_eligibleis the only field to build on. It istrueonly when every check passed. When it isfalse,fieldsis an empty object andportraitisnull, whatever the model read.eligibility_reasonnames the first failing check andeligibility_messageis a sentence you can show the end user unchanged.gate_rejectionslists every failing check, not just the first, so a scan that is both expired and unreadable on one line reports both.raw_textis the recognised text per side, for debugging and review. Do not parse it yourself: it is exactly what the eligibility gate already judged insufficient.portraitis the cropped photo from the document, as a signed URL when storage is on and inline base64 otherwise.
Eligibility reasons
eligibility_reason | What to tell the user |
|---|---|
eligible | All required checks passed. |
unsupported_document_type | The document could not be identified as a supported Bhutanese identity document. |
unsupported_document_subtype | This document type is not accepted for identity verification. |
non_bhutanese_passport | Only Bhutanese passports are accepted. |
missing_required_side | Upload both the front and the back. |
insufficient_image_quality | Too blurry or too small; retake in good light, filling the frame with the card. |
monochrome_image | The image is black and white; take a colour photo of the physical document. |
orientation_unresolved | Photograph the card upright and square to the camera. |
missing_required_field | One or more required fields could not be read. |
invalid_field_format | A field was read but its value is not in the expected format. |
invalid_date, future_date, invalid_chronology | A date could not be read, is in the future, or the birth, issue and expiry dates are inconsistent. |
expired_document | The document has expired; a renewed document is required. |
invalid_mrz | The passport's machine-readable zone failed its checksums. |
side_mismatch | The front and back are from different cards. |
side_binding_unavailable | The photos on the front and back could not be matched; retake both sides. |
field_conflict | The same field was read with conflicting values. |
suspected_tamper | The document shows signs of tampering. |
unreadable_document | No readable text was found. |
insufficient_evidence | Not 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.
| Code | Meaning |
|---|---|
unreadable_document | No readable OCR text was produced. |
unsupported_document_type / unsupported_document_subtype | Not a supported document, or a permit subtype that is not accepted. |
missing_required_side:front / :back | The document type requires that side. |
orientation_unresolved | The orientation could not be resolved with confidence. |
insufficient_image_quality:front / :back | That side failed the resolution or blur gate. |
monochrome_image:front / :back | That side has no colour: a grayscale scan or photocopy. |
side_binding_unavailable | The front and back could not be positively matched. |
side_mismatch | The 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_unresolved | Conflicting values for a field. |
invalid_date:<date field> / future_date:<date field> / invalid_chronology | Date problems. |
expired_document | The expiry date is before the evaluation date. |
missing_document_evidence | The issuer and document title were not found on the front. |
non_bhutanese_passport / invalid_mrz / missing_visual_mrz_agreement / missing_place_of_birth_polygon | Passport-specific checks. |
suspected_tamper | An anti-tamper signal vetoed eligibility. |
eligibility_disabled | The 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, forresult_ttl_days.storedandexpires_atin 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, forsource_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.