Errors
Errors use a consistent JSON envelope, machine-readable status codes, and — for verification outcomes — stable reason strings you can branch on.
Error envelope
All errors return a JSON object with a detail field. For request-validation
failures (422 from body parsing) detail is an array of field-level errors; for
everything else it is a human-readable string that doubles as a machine-checkable message.
{
"detail": "missing scope: verifications:write"
}{
"detail": [
{"loc": ["body", "name"], "msg": "ensure this value has at least 2 characters", "type": "value_error.any_str.min_length"}
]
}Status codes
| Code | Meaning | Common causes |
|---|---|---|
400 | Bad request | Malformed request the router could not process. |
401 | Unauthenticated | invalid API key (missing/unknown/revoked X-Api-Key); invalid or expired client token (bad or 30-min-expired X-Client-Token). |
403 | Insufficient scope | Key is valid but lacks a scope: missing scope: <scope>. Re-create the key with the scope or use *. |
404 | Not found | Unknown id — or a resource owned by a different API key (existence is never leaked): verification not found, api key not found, webhook not found. |
409 | State conflict | cannot submit from status <status> — submit requires selfie or doc_back. Check GET /v1/session/state. |
413 | Payload too large | payload too large — /v1/signals/collect bodies are capped at 64 KB. |
422 | Unprocessable | Validation failures: invalid base64 image, consent is required to proceed, barcode could not be parsed, image_b64 required…, video_b64 or frames_b64 required, field-level validation arrays. |
429 | Rate limited | Free tier exhausted: free tier rate limit exceeded — 30 lookups per hour. Honor the Retry-After header (seconds). |
500 | Server error | Unexpected server-side failure. Retry with backoff; contact support if persistent. |
Idempotent GETs are safe to retry on 429/5xx with exponential backoff. POST /v1/verifications creates a new resource per call — reconcile with your external_ref if you are unsure whether a create succeeded.
Decision reason strings
A decided verification carries decision_reasons — stable, snake_case strings
explaining every point lost and every override applied. They are safe to switch on in code.
| Reason | Meaning / remediation |
|---|---|
barcode_parse_failed | The PDF417 payload could not be parsed as AAMVA CDS. Re-capture the back image in better light. |
mandatory_elements_missing | Required AAMVA data elements were absent. Likely a worn/damaged card or unsupported encoding. |
document_expired | The document's expiry date is in the past. |
license_number_recompute_failed | FL/IL/WI license number did not recompute from name/DOB/sex — strong forgery indicator. |
server_client_barcode_mismatch | Independent server re-decode differs from the client-supplied barcode — tamper signal. |
server_barcode_decode_unavailable | Server-side decode degraded (neutral: full points, but noted). |
face_match_below_threshold | Selfie vs license portrait similarity < 0.6. |
face_match_unavailable | Face-match ML was unavailable; decision is capped at review. |
liveness_below_threshold | Passive liveness score < 0.5 — possible presentation attack. |
liveness_unavailable | Liveness ML was unavailable; decision is capped at review. |
ml_degraded_capped_at_review | Score qualified for approve but a degraded ML signal forced review. |
ofac_match | Strong OFAC SDN match → automatic decline. |
ofac_possible_match | Possible OFAC match → capped at review. |
ofac_screen_unavailable | Screening could not run; capped at review (never fails open). |
face_duplicate_different_identity | This face was previously verified under a different name → decline. |
face_seen_before | This face appears in prior verification/fleet data → capped at review. |
face_dedupe_unavailable | Duplicate-face check could not run; capped at review. |
The face_dedupe check can additionally report non-blocking reasons
(returning_customer, below_threshold, no_matches,
unavailable) in doc_checks.face_dedupe.reason; only the two above reach
the decision. Scoring weights and bands are documented in
the decision object.