Verify a US driver's license
Verify a US driver's license or state ID with the PDF417 barcode (AAMVA), each state's license number format, expiration and minimum age, with Node or Python.
Age-gated sign-ups (alcohol delivery, venues, 21+ events) and rentals (cars, equipment, vacation homes) usually ask the customer for a photo of their driver's license or state ID card. You want to know three things: who the person is, whether they are old enough, and whether the document is still valid.
Constaia has two types for these documents: us_driver_license (all 50 states and the District of Columbia, including
commercial CDLs and learner permits) and us_state_id. With expect you get a verdict in which age, expiration,
number format and the barcode-to-front cross-check have already been evaluated.
The flow
The customer uploads the license
Your frontend (web or mobile) sends the image to your backend, with the front and the back in the same file:
the back carries the PDF417 barcode. The API key never leaves your server. If you use the
upload widget, sides="2" captures both sides and merges them into a single JPEG, so the analysis
costs 1 credit.
Your backend asks Constaia to verify it
You call POST /v1/analyze with expect: ["us_driver_license", "us_state_id"] and checks.min_age_years: 21.
Constaia extracts the fields, reads the barcode, runs the checks and returns a verdict.
Your code decides
valid is accepted, invalid is rejected and review goes to a person. The specific reasons are in
verdict.reasons and checks[].
What Constaia checks
| Check | Where it shows up | What it does |
|---|---|---|
| PDF417 barcode (AAMVA) | checks[]: aamva_matches_visual | Reads the barcode on the back, parses the AAMVA data, fills in fields missing from the front and cross-checks number, name, date of birth and expiration against the printed data. |
| License number | checks[]: id_number_format | Validates the number format for the issuing state (us_dl scheme using issuing_state). |
| ZIP code | checks[]: id_number_format | Validates the 5- or 9-digit ZIP (us_zip scheme). |
| Expiration | verdict.reasons: not_expired | On by default for these types. |
| Minimum age | verdict.reasons: age or min_age_years | Only if you send min_age_years (for example 21). |
| Holder | verdict.reasons: holder | Only if you send holder with the name, number or date of birth you expect. |
The barcode is read on Constaia's server, without sending it to any external provider, and costs nothing extra: the
analysis is still 1 credit per document of up to 2 pages. If a barcode value does not match the printed one,
aamva_matches_visual comes back with passed: false, its message names the mismatched fields, and those fields carry
validated: false. A failed item in checks[] adds an error reason with the same code to the verdict.
No back, no barcode cross-check
If the file only has the front, Constaia extracts the printed fields and runs the other checks, but there is no
aamva_matches_visual in checks[]. Ask for both sides in one image or a 2-page PDF, or use the widget with
sides="2". If your policy requires it, treat a missing barcode check as a reason for review.
The us_driver_license fields include issuing_state, document_number, first_name, middle_name, last_name,
name_suffix, birth_date, issue_date, expiry_date, sex, height, weight, eye_color, hair_color,
address, postal_code, real_id_compliant, dd, organ_donor, veteran, under_21_until, class,
restrictions, endorsements, commercial and permit_type. The full list and the checks for each type are in the
document catalogue and in
GET /v1/document-types/us_driver_license.
Request with curl
curl https://api.constaia.com/v1/analyze \
-H "Authorization: Bearer $CONSTAIA_API_KEY" \
-F file=@license.jpg \
-F 'options={
"expect": ["us_driver_license", "us_state_id"],
"checks": { "min_age_years": 21 },
"storage": "none",
"keep_results": false,
"language": "en"
}'Response (trimmed):
{
"status": "completed",
"document": { "type": "us_driver_license", "label": "US driver's license", "confidence": 0.97, "side": "both", "country": "USA" },
"verdict": {
"expected": ["us_driver_license", "us_state_id"],
"match": true,
"status": "valid",
"reasons": [
{ "code": "type_match", "severity": "info", "message": "The document is US driver's license." },
{ "code": "not_expired", "severity": "info", "message": "Valid until 30/07/2031." },
{ "code": "age", "severity": "info", "message": "The holder is 41 years old." },
{ "code": "i9_list", "severity": "info", "message": "Acceptable Form I-9 document: List B." }
]
},
"fields": {
"issuing_state": { "value": "CA", "confidence": 0.99, "validated": null, "source": null },
"document_number": { "value": "I1234568", "confidence": 0.98, "validated": true, "source": null },
"birth_date": { "value": "1985-07-30", "confidence": 0.99, "validated": null, "source": null },
"expiry_date": { "value": "2031-07-30", "confidence": 0.99, "validated": null, "source": null },
"real_id_compliant": { "value": true, "confidence": 0.93, "validated": null, "source": null }
},
"checks": [
{ "code": "id_number_format", "passed": true, "message": "Identifier document_number (us_dl) is valid." },
{ "code": "id_number_format", "passed": true, "message": "Identifier postal_code (us_zip) is valid." },
{ "code": "aamva_matches_visual", "passed": true, "message": "The PDF417 barcode (AAMVA v10) was read and matches the printed data." }
],
"warnings": []
}Each value arrives as fields.<name>.value, with confidence (0 to 1), validated (true or false when a
deterministic check evaluated it) and, when available, the source page and bounding box in source.
Decide in your code
npm i @constaia/sdkimport { Constaia, ConstaiaError } from "@constaia/sdk";
import { fromPath } from "@constaia/sdk/node";
const constaia = new Constaia(); // reads CONSTAIA_API_KEY
const MIN_AGE = 21;
type Decision = {
decision: "approve" | "reject" | "manual_review" | "pending";
reasons: string[];
state?: string;
};
export async function verifyLicense(path: string, expectedName?: string): Promise<Decision> {
const analysis = await constaia.analyze(await fromPath(path), {
expect: ["us_driver_license", "us_state_id"],
checks: {
minAgeYears: MIN_AGE,
...(expectedName ? { holder: { fullName: expectedName } } : {}),
},
storage: "none",
keepResults: false,
language: "en",
metadata: { flow: "age_gate_21" },
});
// If it takes longer than 30 s the API answers 202; with keepResults: false the result only arrives by webhook.
if (analysis.status !== "completed" || !analysis.verdict) {
return { decision: "pending", reasons: [analysis.id] };
}
const { status, reasons } = analysis.verdict;
const errors = reasons.filter((r) => r.severity === "error").map((r) => `${r.code}: ${r.message}`);
const warnings = reasons.filter((r) => r.severity === "warning").map((r) => `${r.code}: ${r.message}`);
const state = analysis.fields.issuing_state?.value as string | undefined;
if (status === "invalid") return { decision: "reject", reasons: errors, state };
if (status === "review") return { decision: "manual_review", reasons: warnings, state };
// Your own policy: require the barcode on the back to have been read.
const barcodeRead = analysis.checks.some((c) => c.code === "aamva_matches_visual" && c.passed);
if (!barcodeRead) return { decision: "manual_review", reasons: ["barcode_not_read"], state };
return { decision: "approve", reasons: [], state };
}
verifyLicense(process.argv[2] ?? "./license.jpg")
.then((result) => console.log(result))
.catch((err) => {
if (err instanceof ConstaiaError) {
console.error(`Constaia error ${err.status} ${err.code} (request ${err.requestId})`);
} else {
console.error(err);
}
process.exit(1);
});CONSTAIA_API_KEY=ck_live_... npx tsx verify-license.ts ./license.jpgAdjust the rules to your policy: some businesses accept an expired license within a grace period (send
not_expired: false and decide yourself from expiry_date), others ask for an additional document. To evaluate age on
another date, such as the day of the event, use reference_date. Keep thresholds in configuration, not scattered
through the code. More in Verdicts and Checks.
Test mode
With a ck_test_ key the simulator has no US-specific scenarios: a driver's license is answered with the generic
scenario, so with expect: "us_driver_license" the verdict is review with type_unknown. That is enough to test
the integration and the manual_review branch. To see real results for US documents, use a live key: the free plan
includes 150 credits a month (free live credits require a verified email). See Test mode.
Accept passports too
If the customer has no driver's license, widen expect with the US passport (book or card). For the book,
us_passport_book, Constaia also checks the MRZ check digits and cross-checks the MRZ with the printed data:
const analysis = await constaia.analyze(await fromPath("./id.jpg"), {
expect: ["us_driver_license", "us_state_id", "us_passport_book", "us_passport_card"],
checks: { minAgeYears: 21 },
storage: "none",
keepResults: false,
});
console.log(analysis.document?.type, analysis.verdict?.status); // e.g. "us_passport_book" "valid"For foreign passports, add the generic passport type.
If you only need a few fields
If you don't need a verdict and only want a few values, you can pass your own JSON Schema as extract without
expect: Constaia returns exactly the properties you define in fields. In that case verdict is null and the
automatic checks (not_expired, min_age_years…) don't apply to your fields. See
POST /v1/analyze.
Privacy and compliance
- DPPA. The federal Driver's Privacy Protection Act limits how personal data from state motor vehicle records is obtained and disclosed. Constaia does not access DMV records: it only processes the image the person provides. It is still sensitive data: collect it only for a clear purpose, tell the customer why, and check with your legal counsel how the DPPA and state laws apply to your case.
- Minimize. Nothing is kept by default with
storage: "none"andkeep_results: false. Store only the decision and the fields you actually need (often "21+ verified" and the date are enough) and never put license numbers inmetadata. More in Analyze without storing and Storage & privacy. - No biometrics. Constaia reads the document; it does not compare the photo with a selfie or do facial recognition,
so it does not process biometric identifiers (BIPA, CUBI). Warnings such as
screen_photo_suspectedoredited_suspectedare signals to review, not proof of fraud. - CCPA. Constaia acts as a service provider: it processes the data only to provide the service to you.
- Where data is processed. Today everything is processed and stored in the EU, including for US customers. A US region is planned, coming soon, with no date yet. See Data residency & compliance.
This guide is not legal advice.
Next steps
US documents
The United States document types in the Constaia catalogue, PDF417 (AAMVA) barcode reading on driver's licenses, Form I-9 lists, and validation of SSN, EIN and other identifiers.
Form I-9 supporting documents
Verify the List A, B and C documents a new hire presents for Form I-9, tag them by list and pre-fill Section 2 for your HR reviewer, with Node or Python.