Recognize a driver licence¶
A driver licence goes to the same endpoint as a passport, with the same body. What differs is where the data is printed. Most issuers put no machine-readable zone on a licence at all. The answer is then assembled from the visual zone and, where one is printed, a barcode.
Send it¶
The call is the same one every document takes. The response reports the zone as a single verdict, so a missing zone is one branch rather than a walk through nulls.
curl
curl -X POST https://api.doc.cheap/v1/scans \
-H "Authorization: Bearer sk_sandbox_public" \
-H "Content-Type: application/json" \
-d "{\"image\": \"$(base64 < document.jpg | tr -d '\n')\"}"JavaScript
import { readFileSync } from "node:fs";
const image = readFileSync("document.jpg").toString("base64");
const response = await fetch("https://api.doc.cheap/v1/scans", {
method: "POST",
headers: {
Authorization: "Bearer sk_sandbox_public",
"Content-Type": "application/json",
},
body: JSON.stringify({ image }),
});
const scan = await response.json();
const classes = scan.fields.filter((field) =>
["dl_class", "permit_class"].includes(field.name),
);
console.log(scan.document?.kind, scan.mrz.status, classes.map((f) => f.value));Python
import base64
import json
import urllib.request
with open("document.jpg", "rb") as file:
image = base64.b64encode(file.read()).decode()
request = urllib.request.Request(
"https://api.doc.cheap/v1/scans",
data=json.dumps({"image": image}).encode(),
headers={
"Authorization": "Bearer sk_sandbox_public",
"Content-Type": "application/json",
},
)
with urllib.request.urlopen(request) as response:
status = response.status
scan = json.load(response)
classes = [f["value"] for f in scan["fields"] if f["name"] in ("dl_class", "permit_class")]
print(scan["document"]["kind"], scan["mrz"]["status"], classes)No machine-readable zone is not a failure¶
Treat absent as an ordinary outcome, not an error.
mrz.status reads absent, and reason, lines and text are all null.
The group itself is present, as every group always is.
absent and failed are different answers. absent means no zone was read, so
there was nothing to check. failed means a zone was read and something in it
did not check out, which is worth acting on.
The rest of the document is unaffected. A licence is recognized off its printed
face like any other document, and meta.status comes back recognized.
{
"mrz": { "status": "absent", "reason": null, "lines": null, "text": null },
"quality": { "overall": "pass" },
"fields": [
{ "id": "dl_class@0", "name": "dl_class", "label": "Driving-licence class",
"category": "document", "value": "B, BE", "language": null, "confidence": "high" },
{ "id": "authority@0", "name": "authority", "label": "Issuing authority",
"category": "document", "value": "THESSALONIKI REGIONAL OFFICE", "language": null, "confidence": "medium" }
]
}Photographing a card¶
A licence is a glossy plastic card, and it is harder to photograph than a passport page.
- Fill the frame with the card, square on. A card at an angle loses the characters nearest the far edge.
- Turn the flash off. A direct flash reflects off the laminate and erases whatever is under the highlight.
- Light it from the side, not from behind the camera.
- Send both sides when the barcode matters, as two calls.
What makes the scan billable without a zone¶
A live key is charged when the engine determined the document type and one of three things is true.
- A machine-readable zone was read and its check digits passed.
- Five or more fields of the visual zone were read.
- A barcode was decoded.
A licence therefore bills on the second or third of those. A photograph too poor to yield five fields and no barcode is free, whatever else went right. What a billed scan is holds the whole rule.
The barcode¶
A decoded barcode gets no group of its own. Its values arrive in fields
beside everything else, and images.barcode carries a crop of it when the
engine cut one out.
Issuers that print a barcode usually put it on the back of the card, and this endpoint reads one image per call. Send the back as its own scan when you need what the barcode carries, and merge the two results yourself.
The fields a licence fills¶
Licence layouts vary by issuer more than passports do, so the field list is open rather than fixed. Any field the engine reads is published, whether or not we have a curated key for it. These are the ones worth looking for by name.
name |
Label | Category |
|---|---|---|
dl_class |
Driving-licence class | document |
permit_class |
Permit / licence class | document |
authority |
Issuing authority | document |
document_number |
Document number | document |
issue_date |
Date of issue | dates |
expiry_date |
Date the licence runs out | dates |
address, address_street, address_city, address_state, address_postal_code |
Address, and its parts | address |
height, eyes_color |
Height, Eye colour | other |
Address parts are published separately as well as whole, because an issuer may
print only some of them. Read whichever your own model needs, and do not assume
address is the sum of the parts.
Key on id, not on name. A licence printed in two scripts returns one entry
per language under the same name, and only id is unique across the array.
Dates and the countdown to the end of validity¶
Every date is ISO 8601. A licence that names the date it runs out also gets
document.days_remaining and a derived days_to_expire field. Both count from
the day of the scan and go negative once the licence has run out. The field
carries its value as a string, the way every entry of fields[] does.
Next¶
- The response — every group, key by key.
- Recognize a passport — the zone layouts, for the licences that do carry one.
- Handle errors — what an HTTP error means here, as
against a
200that recognized nothing.