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:

TypeNotes
passportPhoto page is read via the MRZ (TD3) plus visual OCR.
id_card / national IDGeneric card family. Legacy id_card accepts any card-family document.
citizenshipNepali citizenship certificate.
voter_idVoter ID card.
pan (PAN card)Recovered by post-classification refinement when needed.
drivers_licenseOften single-sided in some jurisdictions (see require_back_side).
residence_permitCard family.
disabilityNo 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_actionhold (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, if allow_bypass is on, a Continue-anyway option). flag does 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 codeOutcomeWhat triggers it
ID_DOCUMENT_IN_BLOCKLISTDeclinedThe document number matches a block-action entry on a typed risk list
Type/structural mismatchDeclinedThe captured image doesn't match the declared document type or fails basic structural checks
Expired (with allow_expired: false)DeclinedExpiry date has passed and the workflow doesn't accept expired documents
document_forgery_suspected / exif_metadata_suspiciousReview (or session-held, under flag)Tamper analysis (TruFor / EXIF rules) flagged the capture
mrz_checksum_failed / no_mrz_detected / mrz_ocr_mismatchReviewMRZ check digits don't reconcile, or the MRZ is missing/unreadable, or MRZ and visual OCR disagree
no_portrait_detectedReviewNo usable face crop found on the document — blocks downstream face match
back_side_missingReviewA two-sided document type was captured front-only
document_type_not_allowedReviewThe presented type isn't in allowed_document_types / the country matrix
watchlist_hitReviewThe document number matched a review-action typed-list entry
document_unreadableReviewNo identity fields and no MRZ recovered at all
Quality floor not metReviewCapture quality too low to trust the read either way

Warning

Common integration pitfalls.

  1. Setting allowed_document_types without also constraining country_documents can leave a mismatch where the console shows one thing and the flow enforces another — the country matrix always wins when both are set.
  2. Enabling forgery_check with forgery_action: "flag" and expecting the document step itself to reflect review — it won't; the step can approve while the session is force-held. Check session.metadata.background_review, not the module status, if you rely on this.
  3. Treating allow_bypass as "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"])

Edge cases