Constaia
Use-case guides

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.

Cette page n'est pas encore traduite dans votre langue. Voici la version anglaise.

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

CheckWhere it shows upWhat it does
PDF417 barcode (AAMVA)checks[]: aamva_matches_visualReads 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 numberchecks[]: id_number_formatValidates the number format for the issuing state (us_dl scheme using issuing_state).
ZIP codechecks[]: id_number_formatValidates the 5- or 9-digit ZIP (us_zip scheme).
Expirationverdict.reasons: not_expiredOn by default for these types.
Minimum ageverdict.reasons: age or min_age_yearsOnly if you send min_age_years (for example 21).
Holderverdict.reasons: holderOnly 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):

Response
{
  "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/sdk
verify-license.ts
import { 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.jpg

Adjust 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:

verify-id.ts
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" and keep_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 in metadata. 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_suspected or edited_suspected are 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

Sur cette page