The response¶
The body POST /v1/scans returns, and the body GET /v1/scans/{id} returns
for a stored scan. One response shape, with no way to ask for another.
The body answers the question a caller has: what does this document say, and can it be trusted. It does not hand over the recognition engine's working notes. Every value is a conclusion — one value per reading, a confidence band, and a single verdict on the machine-readable zone.
Every key is present. A value that is not known is null, never a missing
key, and a collection that is empty is []. A consumer can read
scan.holder.surname after one null check on holder, never a chain of them.
The eight groups¶
| Group | Type | What it carries |
|---|---|---|
meta |
object | The scan itself: its id, outcome, billing, timing and revision |
document |
object or null | What the document is, and whether it is still valid |
holder |
object or null | The person the document is about |
fields |
array | Every field read off the printed document, re-keyed |
mrz |
object | The machine-readable zone as a verdict, with its lines |
images |
object | Seven image slots |
quality |
object | Whether the picture was good enough to recognize from |
authenticity |
object | Authenticity verification, when it runs |
Every example on this page is one holder: Eleni Sofia Paradeigma, an invented Greek national whose documents are invented with her. The values below are hers throughout the documentation.
{
"meta": {
"schema_version": "1.0",
"id": "01a0af18-cd8d-7a61-9f2d-4c7b8e105da3",
"status": "recognized",
"billed": true,
"confidence": "high",
"timing": { "upload_ms": 198, "processing_ms": 812, "total_ms": 1024 },
"created_at": "2026-09-17T10:15:00Z",
"reference": "order-1042"
},
"document": {
"kind": "passport",
"country": "GRC",
"country_name": "Greece",
"issuing_state": "GRC",
"type_name": "Greece - Passport",
"type_confidence": "high",
"is_expired": false,
"days_remaining": 2001
},
"holder": {
"given_names": "ELENI SOFIA",
"surname": "PARADEIGMA",
"full_name": "PARADEIGMA ELENI SOFIA",
"birth_date": "1994-03-08",
"sex": "F",
"nationality": "GRC"
},
"fields": [
{ "id": "surname@0", "name": "surname", "label": "Surname", "category": "identity", "value": "PARADEIGMA", "language": null, "confidence": "high" },
{ "id": "surname@1032", "name": "surname", "label": "Surname", "category": "identity", "value": "ΠΑΡΑΔΕΙΓΜΑ", "language": "Greek", "confidence": "high" }
],
"mrz": {
"status": "passed",
"reason": null,
"lines": [
"P<GRCPARADEIGMA<<ELENI<SOFIA<<<<<<<<<<<<<<<<",
"AM73045184GRC9403084F3203101PN48291630<<<<72"
],
"text": "P<GRCPARADEIGMA<<ELENI<SOFIA<<<<<<<<<<<<<<<<AM73045184GRC9403084F3203101PN48291630<<<<72"
},
"images": {
"document_crop": "data:image/jpeg;base64,…",
"rear": null,
"main_photo": "data:image/jpeg;base64,…",
"signature": "data:image/jpeg;base64,…",
"watermark_face": null,
"barcode": null,
"chip": null
},
"quality": { "overall": "pass" },
"authenticity": { "overall": "not_checked", "checks": [] }
}fields is truncated above to two entries; a real passport produces about
twenty.
meta¶
| Key | Type | Meaning |
|---|---|---|
schema_version |
string | Always "1.0" |
id |
string | The scan id |
status |
string | How far recognition got |
billed |
boolean | Whether the scan drew a credit |
confidence |
band | How strongly the recognition as a whole is backed |
timing |
object or null | The split of the request's time |
created_at |
string | ISO 8601 timestamp in UTC |
reference |
string or null | The request's own correlation string |
schema_version¶
The revision of this body. A removal or a rename moves its major segment. A consumer that pins the value therefore fails loudly, rather than quietly reading a key that changed meaning.
It is not the /v1 in the path. The path names the API; this names the shape of
the body under it.
id¶
A UUID version 7 in canonical lower-case 8-4-4-4-12 form, as defined by
RFC 9562. Its leading 48 bits are the millisecond the scan was made, so ids sort
as strings in the order the scans happened.
Treat the value as opaque. Nothing else about it is part of the contract.
status¶
Exactly these five strings; no numeric codes.
| Value | Meaning |
|---|---|
recognized |
The document type was determined and data came out of it |
no_document_found |
Nothing document-shaped was in the frame |
unreadable |
A document was there, but no usable text was read off it |
unsupported_document |
A document was found, but its type is not a known one |
rejected |
Recognition failed on the service's side |
All five arrive under HTTP 200. A failure to recognize is an outcome, not an error.
billed¶
Whether this scan drew a credit. recognized and billed are separate facts: a
scan can be billed without being recognized, and the rule that decides is on
what a billed scan is.
timing¶
| Key | Measures |
|---|---|
upload_ms |
From the request's headers arriving to its body being received and validated. The key is resolved and its rate limit checked inside this number; the allowance, idempotency and credit gates are not |
processing_ms |
The recognition itself, the engine call |
total_ms |
From the request arriving to the result being complete |
total_ms is at least upload_ms + processing_ms. The remainder is the gates
that run once upload_ms is taken: the allowance, the idempotency check and
the credit hold. Preparing the result images and mapping the engine's output
into this body are in it too.
It stops there. Writing the history row and serializing the response happen after the number is fixed, so the stored figure and the returned figure are the same.
The split separates the caller's own network from the service's engine. timing
is null on a scan stored before the split was measured and read back later.
document¶
| Key | Type | Meaning |
|---|---|---|
kind |
string | Document type, for example passport |
country |
alpha-3 or null | Country of the document |
country_name |
string or null | That country's name |
issuing_state |
alpha-3 or null | Issuing state |
type_name |
string or null | The full type name, for example Greece - Passport |
type_confidence |
band | How strongly the type match is backed |
is_expired |
boolean or null | Whether the document had expired at the time of the scan |
days_remaining |
integer or null | Days until expiry; negative once expired |
type_name carries no catalogue ordinal. The recognition engine appends a
number to distinguish two catalogue entries that print the same name, and a
trailing " #2" is stripped before publication. That number says nothing about
the document in front of the camera.
type_name is null on a scan read back from storage. Only the live engine
result carries it, and no engine result is kept.
document as a whole is null when no document was identified.
holder¶
| Key | Type |
|---|---|
given_names |
string or null |
surname |
string or null |
full_name |
string or null |
birth_date |
date or null |
sex |
M, F, X or null |
nationality |
alpha-3 code or null |
A name never contains a line break. A document that prints the holder's names across two lines yields one value with a single space between them, in every script. A scan stored before that rule existed is repaired when it is read back.
holder carries the merged Latin reading. The national-script spelling is an
entry of fields, not a second set of keys here.
fields¶
Every field read off the printed document, re-keyed to this API's own
vocabulary. The set is open: a field the vocabulary does not name is still
published, with a slugged key and the category other.
| Key | Type | Meaning |
|---|---|---|
id |
string | Identity of this entry, unique across the array |
name |
string | The stable snake_case key |
label |
string | The human label |
category |
string | identity, document, dates, address, visa or other |
value |
string or null | The value of this reading |
language |
string or null | The language this reading was made in |
confidence |
band | How strongly this reading is backed |
name repeats; id does not. A document that prints a field in two scripts
yields one entry per script, all under the same name. id is name@lcid —
the key, an @, and the numeric language identifier — with #2, #3 appended
when the same pair appears twice.
The whole catalogue, the open-set rule and the two field types withheld on purpose are on the field catalogue. The language identifiers are on field languages and scripts.
The same value in two places¶
A value can appear twice in one body: once in holder or document, and once
in fields. They are not duplicates of one another.
| Where | What it carries |
|---|---|
holder, document |
The merged best reading, in a fixed set of keys |
fields |
One entry per reading, including the national-script one |
A consumer that wants a name reads holder. A consumer that wants the Greek
spelling reads the fields entry whose language is Greek.
Confidence is a band¶
Every confidence in this body is one of three words, never a number.
| Band | The engine reported |
|---|---|
high |
90 or above, out of 100 |
medium |
60 up to 90 |
low |
Below 60, or no probability at all |
A missing probability reads as low rather than being absent, so the key is
always answerable. meta.confidence and document.type_confidence are scored
from 0 to 1. They are cut at the same two points, 0.9 and 0.6, so high means
the same thing wherever it appears.
Bands rather than numbers, because a recognition probability is not a calibrated
percentage. A consumer that saw 97 would build a threshold on it that means
nothing.
mrz¶
| Key | Type | Meaning |
|---|---|---|
status |
passed, failed or absent |
The verdict |
reason |
string or null | One sentence naming what did not check out; null unless failed |
lines |
array of strings or null | The zone's lines in order, exactly as read |
text |
string or null | The same lines run together, one unbroken string |
passed means the zone is present, every check digit validated, and nothing in
it contradicts the printed page. failed means the zone is present and
something did not check out. absent means the document carries none.
reason names a subject whenever it can. When the aggregate fails with no field
to blame, the sentence is exactly MRZ check digits did not validate.
lines and text travel with the verdict rather than instead of it. They are
the only part of the response that can be independently re-verified. The formats
and the per-digit detail are on the MRZ reference.
images¶
Seven slots, each a data: URL or null.
| Key | What it is |
|---|---|
document_crop |
The document cropped out of the uploaded picture and deskewed |
rear |
The rear side, when one is in the frame |
main_photo |
The holder's photograph as printed |
signature |
The signature strip |
watermark_face |
The faint second copy of the holder's face printed into the page as a security feature |
barcode |
The barcode region |
chip |
The chip symbol |
main_photo is null when the request sent return_portrait: false. The other
six are unaffected by that option.
Crops are never stored, so a scan read back from storage carries all seven as
null. The height caps and the re-encode are on
result images.
quality¶
| Key | Type |
|---|---|
overall |
not_checked, pass, warn or fail |
One verdict on whether the uploaded picture was good enough to recognize from. The value is the worst thing any check said about it. A single failed check is a failed picture, whatever the engine's own aggregate read.
not_checked is a distinct answer from pass. It is what a result read back
from storage carries, because a stored result keeps no engine output. Reporting
that as a pass would vouch for a picture the process never saw.
No per-check breakdown is published. The engine's own check types are integers
with no verified name map, so a breakdown handed a reader check_7: fail and
nothing to act on.
authenticity¶
| Key | Type |
|---|---|
overall |
not_checked, pass, warn or fail |
checks |
array of check objects |
Under the recognition-only scenario overall is not_checked and checks is
empty. The group is present so a consumer's parsing does not change when
authenticity verification is populated.
A member of checks carries name, label, result (pass, warn or
fail) and detail, which is a string or null.
What changes when a scan is read back¶
GET /v1/scans/{id} returns the stored result. It differs from the recognition
response in exactly four places, and all four have the same cause: no engine
output is kept.
| Key | On a read-back |
|---|---|
images |
All seven slots null |
quality.overall |
not_checked |
document.type_name |
null |
meta.timing |
The stored figures, or null on a scan stored before they were measured |
Everything else is the body that was returned at the time. The read is never billed, and it never re-runs recognition.
Reading it safely¶
Four rules cover the shapes a consumer meets.
- Check the group, not the key.
document,holderand the scalar values inside them can each benull; the groups themselves are the null checks worth writing. - Key
fieldsbyid, never byname. - Branch on
statusbefore reading data. Four of the five outcomes carry little or nothing. - Pin
meta.schema_version. A consumer that asserts"1.0"learns about a change from its own tests rather than from a wrong value in production.
Related¶
- Scan options — what a request can set.
- POST /v1/scans — the endpoint, its headers and every status it answers with.
- Versioning — what a breaking change would be.