Verification
Document verification
OCR, MRZ and barcode extraction, authenticity and tamper checks, and the country/type document matrix.
The ID_VERIFICATION module authenticates a government-issued identity document, extracts its fields, and outputs the document portrait so face match can compare it to the selfie. It runs inside the hosted flow: the applicant photographs the document (front, and back when required), and the engine does OCR, machine-readable-zone parsing, and authenticity analysis server-side.
Info
This is usually the first module in a KYC workflow — every other identity module (face match, NFC, proof of address risk-scoring) depends on data it produces: the document portrait, the extracted fields, or the MRZ used to derive the NFC chip key.
Supported document families
The engine treats these families as first-class types, each with its own classifier label and field template:
| Type | Notes |
|---|---|
passport | Photo page is read via the MRZ (TD3) plus visual OCR. |
id_card / national ID | Generic card family. Legacy id_card accepts any card-family document. |
citizenship | Nepali citizenship certificate. |
voter_id | Voter ID card. |
pan (PAN card) | Recovered by post-classification refinement when needed. |
drivers_license | Often single-sided in some jurisdictions (see require_back_side). |
residence_permit | Card family. |
disability | No trained template — runs a generic extraction (auto-detect fields + best-effort OCR text, clearly flagged). |
Note
Aadhaar cards have no dedicated classifier class, so they first surface as national_id / id_card and are relabelled to aadhaar from OCR-text signals (the 4-4-4 grouped number, UIDAI captions). This is display-level only.
What the engine extracts
Fields are read from the MRZ where present and from visual OCR otherwise:
- Full name (and first / last where the document splits them)
- Document number
- Date of birth, date of expiry
- Nationality / issuing country
- The document portrait (for face match)
- On non-Latin documents, a romanised / English read of the same fields (
translated_fields)
MRZ parsing (passports and other TD1/TD3 documents) cross-checks the printed data against the MRZ check digits. Barcode / PDF417 data, where a document carries it, is decoded for the same fields.
Authenticity and tamper checks
Beyond reading the document, the engine runs forgery / tamper analysis (EXIF-rule checks plus TruFor tamper localisation on the hosted forensics engine). Two config keys control the outcome:
forgery_check— enable tamper analysis.forgery_action—hold(default) routes a suspected forgery to manual review and gates the applicant at the document step (they see a "couldn't verify this document" screen and, ifallow_bypassis on, a Continue-anyway option).flagdoes not gate the applicant — the document step can still approve so they proceed normally — but silently force-holds the whole session for review (session.metadata.background_review) so an operator sees the finding without tipping off a fraudster mid-flow.
Warning
A suspected forgery is never auto-declined by the client. Depending on forgery_action it either caps the module at review or is flagged for an operator — the authoritative outcome still comes from the webhook / decision endpoint. The forgery finding itself (document_forgery_suspected, exif_metadata_suspicious) is also stripped from the applicant-facing response either way — only the operator's console view shows it, so a genuine forger can't learn what tripped the check.
Restricting which documents are accepted
How scoring and decisioning works
ID_VERIFICATION never outputs a single numeric "pass score" the way a credit-risk model might — the terminal status is derived from a set of boolean checks plus a quality_score, evaluated in order:
Blocklist check first
The extracted document number is screened against typed lists. A block-action match on the document list type declines immediately, regardless of everything else.
Hard-fail checks
If any structural check fails (document not recognized, wrong type presented) or the document is expired with allow_expired off, the step declines.
Review-routing warnings
A defined set of non-fatal findings — MRZ checksum failure, no MRZ detected, MRZ↔visual mismatch, implausible dates, screen-capture suspected, no portrait detected, back side missing, forgery suspected (under hold), watchlist hit on a review-action list, issuing-country mismatch — route the step to review rather than an outright decline. These are treated as "OCR noise or a legitimate-but-unclear capture," not proof of fraud.
Quality floor
A capture whose overall quality_score falls below a low internal floor also routes to review even with no other warning — the read is judged too unreliable to trust either way.
Otherwise, approve
No blocklist hit, no hard-fail, no review warning, adequate quality → approved.
Note
An unreadable document — no OCR fields and no MRZ recovered at all (e.g. a genuinely out-of-model document type, or a truly unusable photo) — is deliberately not auto-declined. The engine can't tell "fraudulent" from "just unreadable," so it routes to review instead of penalizing a legitimate holder for a bad-model type or a poor photo.
Common decline / review reasons
| Reason code | Outcome | What triggers it |
|---|---|---|
ID_DOCUMENT_IN_BLOCKLIST | Declined | The document number matches a block-action entry on a typed risk list |
| Type/structural mismatch | Declined | The captured image doesn't match the declared document type or fails basic structural checks |
Expired (with allow_expired: false) | Declined | Expiry date has passed and the workflow doesn't accept expired documents |
document_forgery_suspected / exif_metadata_suspicious | Review (or session-held, under flag) | Tamper analysis (TruFor / EXIF rules) flagged the capture |
mrz_checksum_failed / no_mrz_detected / mrz_ocr_mismatch | Review | MRZ check digits don't reconcile, or the MRZ is missing/unreadable, or MRZ and visual OCR disagree |
no_portrait_detected | Review | No usable face crop found on the document — blocks downstream face match |
back_side_missing | Review | A two-sided document type was captured front-only |
document_type_not_allowed | Review | The presented type isn't in allowed_document_types / the country matrix |
watchlist_hit | Review | The document number matched a review-action typed-list entry |
document_unreadable | Review | No identity fields and no MRZ recovered at all |
| Quality floor not met | Review | Capture quality too low to trust the read either way |
Warning
Common integration pitfalls.
- Setting
allowed_document_typeswithout also constrainingcountry_documentscan leave a mismatch where the console shows one thing and the flow enforces another — the country matrix always wins when both are set. - Enabling
forgery_checkwithforgery_action: "flag"and expecting the document step itself to reflect review — it won't; the step can approve while the session is force-held. Checksession.metadata.background_review, not the module status, if you rely on this. - Treating
allow_bypassas "the applicant can force an approval" — it can't. Bypass only lets them proceed past a blocking screen; the terminal status (review/decline) doesn't change.
Risk-list screening
The extracted document number is screened against the tenant's risk lists (identifier type). A matching list entry can route the session to review, open a case, or block it depending on the list's configured action.
Result shape
ID_VERIFICATION returns the extracted fields, the detected_type and its confidence, an is_document_valid flag, and whether a portrait was found. Retrieve it from GET /v3/session/<id>/ (full result) or in summary form from decision_v3.id_verifications on the decision endpoint.
{
"feature": "ID_VERIFICATION",
"status": "approved",
"score": 91.0,
"warnings": [],
"data": {
"fields": {
"full_name": "AARAV SHARMA",
"document_number": "PA1234567",
"date_of_birth": "1990-04-12",
"expiry_date": "2032-01-10",
"nationality": "NP",
"issuing_state": "NP"
},
"translated_fields": {},
"detected_type": "passport",
"confidence": 0.97,
"is_document_valid": true,
"has_portrait": true
}
}Fetch it directly:
curl -s "$TF_BASE_URL/v3/session/$SESSION_ID/" \
-H "x-api-key: $TF_API_KEY" \
| jq '.results[] | select(.feature == "ID_VERIFICATION")'const res = await fetch(`${process.env.TF_BASE_URL}/v3/session/${sessionId}/`, {
headers: { "x-api-key": process.env.TF_API_KEY! },
});
const session = await res.json();
const idResult = session.results.find((r: any) => r.feature === "ID_VERIFICATION");
console.log(idResult.status, idResult.data.fields);import requests
session = requests.get(
f"{BASE_URL}/v3/session/{session_id}/",
headers={"x-api-key": API_KEY},
timeout=15,
).json()
id_result = next(r for r in session["results"] if r["feature"] == "ID_VERIFICATION")
print(id_result["status"], id_result["data"]["fields"])