>
Module 01
Fintech AI

Identity and Onboarding

Every regulated financial product starts by proving who the customer is. This module covers the full pipeline — quality gating, preprocessing, OCR, field extraction, validation, face match and liveness — with the tools that exist today, working code, and the failure modes that decide whether it ships.

Verified May 2026Free · No signupOfficial sources only
BeginnerStart here. No prior knowledge assumed.
Product guide

Need the number to stop existing in your systems? The eight steps, what the Aadhaar Data Vault actually requires, and where it has already leaked: Government ID Masking: How to Build It →

Product guide

Deciding whether to let a business accept payments through you? The eight steps, the 2025 PA Directions, and the marketplace rule that makes you answerable for sellers you never onboarded: Merchant Onboarding: How to Build It →

Build sheet

Every tool for this module, how to use each one, what it costs, the best combinations and three recommended builds: Identity Build Sheet →

Product guide

Building document reading as a product? Step-by-step, with the options at each stage and how to connect them: Document AI: How to Build It →

Product guide

Building video KYC as a product? The eight steps, the options at each stage, and what RBI actually requires: Video KYC: How to Build It →

What identity verification actually is

Before a bank, lender or broker can give you an account, it has to answer three questions with evidence:

  1. Does this person exist? Is the identity real, and does the document refer to a real registered person?
  2. Is this person who they claim to be? Is the human in front of the camera the same human on the document?
  3. Should we do business with them? Are they sanctioned, politically exposed, or already known for fraud?

This is Know Your Customer. Every regulated financial product starts here, and nothing else in your product matters until it works.

Note

The three questions need completely different technology. Question one is document reading and database lookup. Question two is face matching and liveness. Question three is name screening against lists. People often assume "KYC" is one thing and buy one tool for it. It is three problems.

Why reading a document is harder than it sounds

You would think reading a name off an ID card is solved. For a clean, flat, well-lit scan of a standard document, it mostly is. Real onboarding does not produce those.

What actually arrives:

  • A photograph of a document, taken at an angle, on a bed, in poor light
  • A photocopy of a photocopy, faded, with a stamp across the text
  • A laminated card with glare covering the date of birth
  • A document in Hindi, Tamil, Bengali or Marathi, sometimes with English on the same card
  • A screenshot of a PDF, re-compressed twice by a messaging app
  • A deliberately altered document, because some share of applicants are committing fraud

Each of these breaks a different part of the pipeline. That is why "just use OCR" does not survive contact with production.

What AI can and cannot do here

TaskHow well AI does it
Read printed text from a clear documentVery well. Effectively solved.
Read a document at an angle, in poor lightWell, with preprocessing. The preprocessing matters as much as the model.
Find the right field on an unfamiliar layoutReasonably. Vision-language models are good at this; older OCR is not.
Read handwritingInconsistently. Better than five years ago, still unreliable for anything consequential.
Match a selfie to a document photoVery well, when both images are decent.
Tell a live face from a photograph held to the cameraWell, against casual attempts. Against a determined attacker with a deepfake, this is an arms race.
Decide whether a document is genuinePartially. It catches crude forgeries. It cannot confirm a document is real — only the issuing database can.
Decide whether to onboard someoneIt should not. That is a business and regulatory decision with a documented reason.
Watch out

The most expensive mistake in this module is treating extraction confidence as identity confidence. A model can be 99% certain it read "RAHUL SHARMA" correctly off a document that is entirely fake. Reading well and verifying are different problems.

The one thing that decides your architecture

In India, you cannot call the Aadhaar verification API directly. Aadhaar eKYC is available only through UIDAI-licensed entities ’ a KYC User Agency or a sub-licensed aggregator. Direct access requires RBI authorisation and a separate licence.

This is the single most important practical fact in this module. A startup cannot buy Aadhaar verification the way it buys cloud hosting. It goes through a licensed intermediary, and that intermediary relationship shapes cost, latency, contract terms and what you are permitted to store.

The same pattern repeats across financial infrastructure. A large share of what you need is indirect access only. Knowing the indirect route is frequently the difference between a product being buildable and not.

The registry in the Intermediate lane marks every entry as direct, indirect or open source for exactly this reason.

IntermediateBuild it. Pipelines, tools and working code.

The pipeline, stage by stage

Nine stages. Most teams build three of them, then spend months debugging the six they skipped.

Document pipeline — the stages that matter
INPUT  photo / scan / PDF page
  |
  1. INTAKE          reject > 10MB, convert HEIC/PDF->image, cap resolution
  |
  2. QUALITY GATE    blur score, brightness, glare, resolution
  |                  -> FAIL FAST. Ask for a re-shoot. This single step
  |                     removes most downstream errors.
  |
  3. PREPROCESS      deskew, de-warp, crop to document edges,
  |                  denoise, contrast normalise
  |
  4. CLASSIFY        which document is this? (Aadhaar / PAN / DL / passport /
  |                  voter ID / utility bill / other)
  |
  5. LAYOUT + OCR    text detection -> recognition, OR a single VLM pass
  |
  6. FIELD EXTRACT   map raw text -> {name, dob, id_number, address, ...}
  |
  7. VALIDATE        checksums, format rules, cross-field consistency,
  |                  date sanity, name-vs-face consistency
  |
  8. CONFIDENCE      per-field score -> auto-accept / review / reject
  |
  9. VERIFY          issuing-database lookup via licensed provider
  |                  (this is the only step that proves the document is real)
  |
OUTPUT structured record + confidence + full audit trail

The quality gate is the highest-return stage

It is also the one most often missing. A blurry photo produces garbage extraction, low confidence, a review queue item, a manual correction and a support ticket. Catching it at capture, while the user still has the document in their hand, costs nothing.

Python — cheap quality gate before any OCR
import cv2, numpy as np

def quality_gate(image_path, min_blur=100.0, min_side=800):
    """Reject bad captures before spending money on OCR.
    Tune thresholds on YOUR OWN sample of real rejected images."""
    img = cv2.imread(image_path)
    if img is None:
        return False, "unreadable_file"

    h, w = img.shape[:2]
    if min(h, w) < min_side:
        return False, "resolution_too_low"

    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    # Variance of Laplacian: low variance = few sharp edges = blurry
    blur = cv2.Laplacian(gray, cv2.CV_64F).var()
    if blur < min_blur:
        return False, "too_blurry"

    mean = gray.mean()
    if mean < 55:
        return False, "too_dark"
    if mean > 205:
        return False, "overexposed"

    # Glare: large saturated region usually sits over a field you need
    saturated = float((gray > 250).sum()) / gray.size
    if saturated > 0.06:
        return False, "glare_detected"

    return True, "ok"

# Returning a SPECIFIC reason matters. "Try again" gets you the same
# bad photo. "Too dark - move to a brighter spot" gets you a usable one.

Preprocessing earns more than model choice

Python — deskew and crop to the document
import cv2, numpy as np

def deskew(gray):
    """Rotate so text lines are horizontal."""
    inv = cv2.bitwise_not(gray)
    thr = cv2.threshold(inv, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
    coords = np.column_stack(np.where(thr > 0))
    if len(coords) < 50:
        return gray
    angle = cv2.minAreaRect(coords)[-1]
    angle = -(90 + angle) if angle < -45 else -angle
    if abs(angle) < 0.4:          # don't rotate for nothing
        return gray
    h, w = gray.shape
    M = cv2.getRotationMatrix2D((w // 2, h // 2), angle, 1.0)
    return cv2.warpAffine(gray, M, (w, h),
                          flags=cv2.INTER_CUBIC,
                          borderMode=cv2.BORDER_REPLICATE)

def crop_to_document(img):
    """Find the largest 4-sided contour and crop to it.
    Removes the bedsheet, the table, the fingers."""
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    edged = cv2.Canny(cv2.GaussianBlur(gray, (5, 5), 0), 50, 200)
    cnts, _ = cv2.findContours(edged, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
    for c in sorted(cnts, key=cv2.contourArea, reverse=True)[:5]:
        peri = cv2.arcLength(c, True)
        approx = cv2.approxPolyDP(c, 0.02 * peri, True)
        if len(approx) == 4 and cv2.contourArea(c) > 0.25 * img.size / 3:
            x, y, w, h = cv2.boundingRect(approx)
            return img[y:y + h, x:x + w]
    return img   # fall back to the original rather than returning nothing

def preprocess(path):
    img = crop_to_document(cv2.imread(path))
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    gray = deskew(gray)
    gray = cv2.fastNlMeansDenoising(gray, h=10)
    return cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)).apply(gray)

Choosing an OCR engine — and why it matters less than you think

The honest comparison, with what each actually breaks on:

EngineTypeStrongest atBreaks onLicence note
Tesseract 5ClassicClean printed text, tiny footprint, fastAngles, low contrast, complex layout, most Indic scriptsApache 2.0
PaddleOCRDeep learningComplex layouts, tables, multilingual; PP-Structure adds layout analysisHeavy dependency; environment/model-loading frictionApache 2.0
docTRDeep learningGood detection+recognition in one pipeline, tunable speed/accuracyLess multilingual coverage than PaddleApache 2.0
EasyOCRDeep learningFastest to a working prototype, 80+ languagesRoughly 3x slower inference; lower accuracy ceiling at volumeApache 2.0
SuryaVLM-styleBest layout analysis in the open group, 90+ languages, dense pagesGPU costCheck this. Code GPL-3.0; model under a licence that is free for research and small companies but requires a commercial licence above a revenue threshold
Qwen2.5-VL / olmOCR / GOT-OCRVision-languageReads contextually, infers structure, handles messy layouts and some handwritingGPU-hungry; can hallucinate plausible text that was never on the pageVaries by model
Cloud document APIs
Google Document AI, AWS Textract, Azure Document Intelligence, Mistral OCR
ManagedNo infrastructure, good accuracy, pre-built ID parsersPer-page cost at volume; data leaves your environment (a hard blocker under data localisation)Commercial

The finding that should change your plan

A controlled 2026 benchmark running eight open-source engines on identical receipt datasets found two things worth internalising.

First, vision-language models are not automatically more accurate on clean printed text. The best traditional engine and the best VLM landed within a hair of each other on character error rate.

Second, and more consequentially: once an LLM post-processor was added, six of the eight engines converged into the same narrow field-accuracy band regardless of engine family. The post-processing step compressed most of the difference between them.

Note

Read that again before choosing an engine. If a cleanup and structuring layer sits after OCR, the engine you pick matters far less than the quality gate before it and the validation after it. Spend your time there. Pick the engine that fits your deployment constraints, not the one at the top of a benchmark.

The specific VLM risk

A vision-language model asked to read a document can produce fluent, well-formatted, entirely invented text. Classic OCR fails visibly, returning garbage characters. A VLM fails invisibly, returning a plausible name that was never on the card.

For identity documents this is the worse failure mode. If you use a VLM, validate every extracted field against the raw detected text or against a checksum. Never accept a VLM field without a second source.

Field extraction — getting structure out of text

OCR gives you text. You need {name, dob, id_number, address}. These are different problems and the second one is where layouts bite.

ApproachHow it worksUse whenWeakness
Regex + anchorsFind a label, take the text near itOne fixed layout you controlBreaks on any layout change; brittle across document versions
Positional templatesFields live at known coordinates after alignmentStandardised cards, high volume, same issuerRequires a template per document type and per version
Layout models (LayoutLM family, Donut)Model learns which text span is which field from position + contentMany layouts, enough labelled examples to fine-tuneNeeds training data; a real ML project
VLM promptingSend the image, ask for JSONUnknown or varied layouts, fast to buildHallucination; cost per page; must validate every field
HybridOCR extracts text, an LLM structures it, validators check itMost production systemsMore moving parts to monitor

The hybrid pattern, which is what most teams end up with

Python — OCR, then LLM structuring, then hard validation
import json, re

EXTRACTION_PROMPT = """You are extracting fields from an identity document.

Below is raw OCR text. Return ONLY a JSON object. No prose, no markdown.

Rules:
- Use ONLY text present in the OCR output. Never infer or complete a value.
- If a field is absent or unreadable, set it to null.
- Do not correct spellings. Return exactly what appears.
- dob must be ISO format YYYY-MM-DD, or null if the format is ambiguous.

Schema:
{"document_type": "aadhaar|pan|passport|driving_licence|voter_id|other",
 "name": string|null,
 "father_name": string|null,
 "dob": string|null,
 "gender": "M"|"F"|"O"|null,
 "id_number": string|null,
 "address": string|null,
 "issue_date": string|null,
 "expiry_date": string|null}

OCR TEXT:
---
{ocr_text}
---"""

def structure_fields(ocr_text, llm_call):
    raw = llm_call(EXTRACTION_PROMPT.replace("{ocr_text}", ocr_text))
    raw = re.sub(r"^```(?:json)?|```$", "", raw.strip(), flags=re.M).strip()
    try:
        fields = json.loads(raw)
    except json.JSONDecodeError:
        return None, "llm_returned_invalid_json"

    # CRITICAL: every returned value must actually appear in the OCR text.
    # This is the guard against a fluent, confident hallucination.
    haystack = re.sub(r"[^A-Z0-9]", "", ocr_text.upper())
    for key in ("name", "id_number", "father_name"):
        val = fields.get(key)
        if not val:
            continue
        needle = re.sub(r"[^A-Z0-9]", "", str(val).upper())
        if needle and needle not in haystack:
            fields[key] = None
            fields.setdefault("_rejected", []).append(key)

    return fields, None
Watch out

The grounding check in the last block is not optional. Without it you are trusting a generative model not to invent a customer identity, and it will eventually invent one.

Validation — the step that catches what extraction missed

Extraction confidence tells you how sure the model is about pixels. Validation tells you whether the value can possibly be correct. The second is far more useful, and it is cheap.

Aadhaar carries a checksum. Use it.

An Aadhaar number is 12 digits with a Verhoeff check digit. A single misread digit fails the checksum. This catches a large share of OCR errors before they reach a human ’ for free, offline, in microseconds.

Python — Verhoeff checksum (Aadhaar) and PAN format validation
# Verhoeff algorithm - dihedral group D5. Catches all single-digit
# errors and all adjacent transpositions, which is exactly the OCR
# failure profile.

_D = [[0,1,2,3,4,5,6,7,8,9],[1,2,3,4,0,6,7,8,9,5],[2,3,4,0,1,7,8,9,5,6],
      [3,4,0,1,2,8,9,5,6,7],[4,0,1,2,3,9,5,6,7,8],[5,9,8,7,6,0,4,3,2,1],
      [6,5,9,8,7,1,0,4,3,2],[7,6,5,9,8,2,1,0,4,3],[8,7,6,5,9,3,2,1,0,4],
      [9,8,7,6,5,4,3,2,1,0]]
_P = [[0,1,2,3,4,5,6,7,8,9],[1,5,7,6,2,8,3,0,9,4],[5,8,0,3,7,9,6,1,4,2],
      [8,9,1,6,0,4,3,5,2,7],[9,4,5,3,1,2,6,8,7,0],[4,2,8,6,5,7,3,9,0,1],
      [2,7,9,3,8,0,6,4,1,5],[7,0,4,6,9,1,3,2,5,8]]

def verhoeff_valid(number: str) -> bool:
    digits = [int(d) for d in str(number) if d.isdigit()]
    if len(digits) != 12:
        return False
    c = 0
    for i, d in enumerate(reversed(digits)):
        c = _D[c][_P[i % 8][d]]
    return c == 0

def aadhaar_valid(number: str) -> bool:
    n = "".join(ch for ch in str(number) if ch.isdigit())
    if len(n) != 12:
        return False
    if n[0] in "01":            # Aadhaar never starts 0 or 1
        return False
    if len(set(n)) == 1:        # 111111111111 etc. - test data
        return False
    return verhoeff_valid(n)

import re
def pan_valid(pan: str) -> bool:
    """AAAAA9999A. 4th char encodes holder type, 5th is surname initial."""
    p = str(pan).strip().upper()
    if not re.fullmatch(r"[A-Z]{5}[0-9]{4}[A-Z]", p):
        return False
    return p[3] in "ABCFGHLJPTK"   # P = individual, C = company, etc.

def gstin_valid(g: str) -> bool:
    return bool(re.fullmatch(
        r"[0-3][0-9][A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]", str(g).strip().upper()))

Cross-field checks find what checksums cannot

Python — consistency rules that catch real errors
from datetime import date

def cross_validate(f: dict) -> list:
    """Returns a list of problems. Empty list = internally consistent.
    None of these prove the document is genuine - they prove it is
    not internally contradictory."""
    issues = []

    dob = f.get("dob")
    if dob:
        try:
            y, m, d = (int(x) for x in dob.split("-"))
            born = date(y, m, d)
            age = (date.today() - born).days / 365.25
            if age < 0:      issues.append("dob_in_future")
            elif age < 18:   issues.append("applicant_is_minor")
            elif age > 110:  issues.append("dob_implausible")
        except Exception:
            issues.append("dob_unparseable")

    if f.get("expiry_date") and f.get("issue_date"):
        if f["expiry_date"] <= f["issue_date"]:
            issues.append("expiry_before_issue")

    name = (f.get("name") or "").strip()
    if name:
        if len(name) < 3:                       issues.append("name_too_short")
        if any(ch.isdigit() for ch in name):    issues.append("digits_in_name")
        if name == name.lower():                issues.append("name_case_suspicious")

    dt = f.get("document_type")
    num = f.get("id_number")
    if dt == "aadhaar" and num and not aadhaar_valid(num):
        issues.append("aadhaar_checksum_failed")
    if dt == "pan" and num and not pan_valid(num):
        issues.append("pan_format_failed")

    return issues

Face match and liveness

Document reading answers "what does this card say". Face match answers "is the person holding it the person on it". They are separate systems and separate vendors more often than not.

Face match

Compare the selfie to the photo extracted from the document. Modern models do this well when both images are usable. The failure cases are predictable: a twenty-year-old passport photo, a heavily compressed document scan, significant weight or facial hair change, and occlusion.

Output is a similarity score, not a yes or no. You choose the threshold, and that choice is a business decision with a fraud-versus-friction trade-off, not a technical default.

Liveness

Passive liveness analyses a single frame or short video for the signals of a real face — texture, depth cues, screen moiré, reflection. No user effort, better completion rates, weaker against a determined attacker.

Active liveness asks the user to blink, turn or follow a prompt. Harder to spoof, and every added instruction loses a percentage of applicants at the exact moment they were about to convert.

Note

India-specific and worth knowing before you pick a vendor: models trained largely on Western datasets underperform on Indian skin tones, low-light interiors and inexpensive Android cameras. Providers that trained on Indian data report meaningfully higher approval rates and fewer false rejections in tier-2 and tier-3 contexts. Test on your actual user base and devices, not on a vendor demo.

The 2026 reality

An RBI Master Direction update in late November 2025 made deepfake and face-liveness maturity, along with regional-language document handling, decisive evaluation criteria for Indian KYC providers. That is not a marketing framing ’ it reflects that injection attacks and synthetic faces moved from theoretical to routine.

Treat liveness as an adversarial control that needs re-testing, not a box that gets ticked at integration.

The tool registry

Marked direct if you can sign up and integrate yourself, indirect if it requires a licensed intermediary, and oss if you host it.

OCR and document extraction

Verified May 2026
Tesseract 5oss
Classic engine, Apache 2.0. Small footprint, fast on clean printed text. Weak on Indic scripts and angled captures.
PaddleOCRoss
Apache 2.0. Strong on complex layouts and multilingual; PP-Structure adds layout analysis. Heavy dependency chain.
docTRoss
Apache 2.0. Detection and recognition in one pipeline, tunable speed/accuracy trade-off.
EasyOCRoss
Apache 2.0. Fastest route to a prototype, 80+ languages. Slower inference, lower ceiling at volume.
Suryaoss
Best layout analysis in the open group, 90+ languages. Licence needs checking — code is GPL-3.0, model licence is free for research and small companies but requires a commercial licence above a revenue threshold.
Qwen2.5-VL / olmOCR / GOT-OCR 2.0oss
Vision-language OCR. Reads contextually, handles messy layouts and some handwriting. GPU-hungry and can hallucinate — always validate output.
Google Document AIdirect
Managed, pre-built identity document parsers, strong accuracy. Per-page cost; data leaves your environment.
AWS Textractdirect
Managed OCR with forms and tables extraction. Same data-residency consideration.
Azure Document Intelligencedirect
Managed, prebuilt ID models, good layout handling.
Mistral OCRdirect
LLM-based document reading API, strong on structure preservation.

India — identity verification and KYC

Verified May 2026
Aadhaar eKYC (UIDAI)indirect
Not directly accessible. Available only through a UIDAI-licensed KYC User Agency or a sub-licensed aggregator. Direct access requires RBI authorisation. This routes almost every fintech through a provider below.
DigiLockerindirect
Government-issued documents pulled with user consent. Reached through licensed partners for most commercial use.
CKYCRindirect
Central KYC Registry — search and upload. Accessed through a licensed entity.
Signzydirect
End-to-end onboarding orchestration, RBI video KYC sandbox experience, CKYCR and DigiLocker integration. Suits banks and NBFCs wanting a full flow rather than parts. Quote-only pricing.
HyperVergedirect
Computer-vision specialist. Face match and liveness models trained on Indian faces and low-end device conditions — materially better approval rates in tier-2/3 contexts than global models. Quote-only pricing.
IDfydirect
Strong OCR and video KYC; bundles Aadhaar, PAN and bank account checks into single workflows. Common in lending and insurance.
Karza / Perfiosdirect
Identity plus financial verification in one stack — bank statement analysis, income checks, fraud signals alongside KYC.
Surepassdirect
Very broad API catalogue (400+ verification endpoints), fast sandbox-to-production. Contact-sales pricing.
AuthBridgedirect
KYC plus wider background screening — employment, education, criminal record. Enterprise-oriented.
Digiodirect
Strongest on the document side — Aadhaar eSign and eStamp. The usual pick when onboarding must end in a signed agreement.
Setudirect
Identity and Account Aggregator infrastructure APIs.
Hypersigndirect
Publishes a public rate card, which is unusual in this market — roughly ₹6 per verification and ₹15 for a full journey with face match at time of writing. Useful as a pricing benchmark even if you buy elsewhere.

Face matching and liveness

Verified May 2026
HyperVergedirect
Liveness and face match tuned for Indian faces, poor lighting and low-cost Android cameras.
AWS Rekognitiondirect
General-purpose face comparison and liveness. Widely used globally; reported to underperform on Indian device and lighting conditions relative to India-tuned models.
Azure Face APIdirect
Face verification and liveness detection. Access is gated — Microsoft restricts the face service and requires an approved use case.
FaceIO / iProov / Incodedirect
Global liveness and anti-spoofing vendors; iProov in particular is oriented to high-assurance and government use.
InsightFace / ArcFaceoss
Open-source face recognition models. Good quality, and you own the entire operational and bias-testing burden.
DeepFaceoss
Python wrapper over several recognition backends. Fine for prototyping, not a compliance-grade liveness solution.
Watch out

Pricing in this market is overwhelmingly quote-only. Public rate cards are rare enough that where one exists it is useful mainly as a benchmark for negotiating elsewhere. Verify current terms directly — this registry records what was publicly visible in May 2026.

A prompt for specifying your own pipeline

Prompt — paste into any AI
You are a senior engineer who has built KYC document pipelines in
production for regulated lenders.

My situation:
- Documents I must handle: [e.g. Aadhaar, PAN, driving licence, utility bills]
- Languages / scripts: [e.g. English, Hindi, Tamil]
- Expected volume: [documents per month]
- Capture channel: [mobile app camera / web upload / agent-assisted]
- Data residency constraint: [e.g. data must remain in India]
- Team size and ML experience: [describe honestly]

Give me:

1. A stage-by-stage pipeline design, saying at each stage whether to
   build, buy, or use open source - and why for MY constraints.
2. A specific OCR engine recommendation with the reason, including
   what it will fail on.
3. The validation rules I must implement for these document types,
   including any checksum or format algorithms.
4. Confidence thresholds to start with, and how to tune them from
   real review-queue data.
5. Estimated cost per 1,000 documents for the buy option versus the
   self-hosted option, stating your assumptions.
6. The three things most likely to go wrong in month one.

Be specific about trade-offs. Where you are uncertain about current
pricing or vendor capability, say so rather than guessing.
AdvancedShip it. Failure modes, thresholds and evidence.

Confidence thresholds and the review queue

Every automated identity system has three outcomes, not two: accept, reject, and send to a human. The third one is where the design work is, and it is the part most teams leave until after launch.

Python — threshold routing with per-field granularity
from dataclasses import dataclass, field

@dataclass
class Decision:
    action: str                    # auto_accept | review | reject
    reasons: list = field(default_factory=list)
    priority: str = "normal"       # review queue ordering

# Thresholds are per FIELD, not per document. An address read at 0.72
# is survivable. An ID number read at 0.72 is not.
FIELD_MIN = {
    "id_number": 0.95,   # checksum-protected, so demand high confidence
    "name":      0.90,   # drives sanctions screening downstream
    "dob":       0.90,   # drives age eligibility
    "gender":    0.75,
    "address":   0.70,   # long, noisy, lower stakes
}

def route(fields, confidences, validation_issues, face_score, liveness_pass):
    d = Decision(action="auto_accept")

    # Hard rejects first - no point queueing these for a human
    if not liveness_pass:
        return Decision("reject", ["liveness_failed"], "high")
    for blocking in ("aadhaar_checksum_failed", "pan_format_failed",
                     "expiry_before_issue", "dob_in_future"):
        if blocking in validation_issues:
            return Decision("reject", [blocking])

    # Soft signals -> human review
    for f_name, floor in FIELD_MIN.items():
        if fields.get(f_name) is None:
            d.action = "review"; d.reasons.append(f"missing:{f_name}")
        elif confidences.get(f_name, 0.0) < floor:
            d.action = "review"; d.reasons.append(f"low_conf:{f_name}")

    if fields.get("_rejected"):        # LLM grounding check fired
        d.action = "review"
        d.reasons.append("ungrounded_llm_field")
        d.priority = "high"

    # Face match: a band, not a line
    if face_score < 0.60:
        return Decision("reject", ["face_mismatch"], "high")
    elif face_score < 0.80:
        d.action = "review"; d.reasons.append("face_borderline")

    if "applicant_is_minor" in validation_issues:
        return Decision("reject", ["under_18"])

    return d

How to actually set the numbers

Do not pick thresholds from a vendor benchmark. Derive them from your own review queue:

  1. Launch deliberately conservative. Route far more to review than you think necessary.
  2. Have reviewers record, for every item, whether the machine was right.
  3. After a few thousand decisions, plot accuracy against confidence. The threshold is the point where machine accuracy meets the accuracy your reviewers actually achieve ’ which is not 100%.
  4. Loosen one field at a time and watch the downstream fraud rate, not just the review volume.
Note

The review queue is also your training data and your best product feedback. Every override is a labelled example and a signal about which document type or capture path is failing. Instrument it from day one — you cannot backfill this.

How this gets attacked

Identity verification is adversarial. A share of your applicants are actively trying to defeat it, and the tooling available to them is cheap.

AttackWhat it looks likeWhat counters it
Print attackPhotograph of a photograph held to the cameraPassive liveness — moiré, texture, reflection; easily caught
Replay attackA recorded video played to the cameraActive liveness with randomised challenges
Deepfake / face swapSynthetic face rendered live over a real feedVendor-side deepfake detection; treat as an arms race, not a fix
Camera injectionVirtual camera feeds a prepared stream, bypassing capture entirelyDevice attestation, SDK integrity checks, refusing virtual camera devices — this is the one most teams miss
Document tamperingEdited photo, altered DOB, substituted portraitTamper detection, font/spacing analysis, and crucially the issuing-database lookup
Synthetic identityReal ID number, fabricated person assembled around itCross-source consistency; bureau and CKYCR history checks
Coerced / mule onboardingA genuine person, genuinely live, opening an account for someone elseNo liveness system catches this. Behavioural and transaction monitoring do — see the Fraud module.
Watch out

Camera injection deserves particular attention. Every control discussed above assumes the image came from the device camera. If an attacker can feed a virtual camera, passive and active liveness both evaluate a perfect synthetic face and pass it. Mobile SDK attestation is the defence, and web capture is structurally weaker than native for this reason.

Note also the last row. The strongest liveness in the world validates that a real person is present ’ not that they are acting freely or for themselves. Mule accounts are opened by real people passing real KYC. That problem belongs to transaction monitoring, not onboarding.

Cost, and where it actually goes

Indicative cost structure per 1,000 documents. Treat the numbers as shape rather than quotation — pricing in this market is almost entirely negotiated.

ApproachCost driverSensible at
Managed cloud document APIPer page, tiered down with volumeLow to moderate volume; no ML team; no data-residency blocker
Self-hosted classic OCR (Tesseract, Paddle)CPU only. Effectively free per page after infrastructureHigh volume, clean documents, strict data residency
Self-hosted VLM OCRGPU hours — the dominant line itemMessy or varied documents where accuracy justifies GPU spend
Indian KYC provider (full journey)Per verification; public benchmarks sit in low double-digit rupees for a full journey with face matchAlmost everyone, because Aadhaar access is indirect anyway
Human reviewReviewer minutesThis is usually the largest line and the one nobody models
Note

Run the arithmetic honestly. If 12% of documents hit the review queue and each takes a reviewer three minutes, that is 36 reviewer-minutes per 100 documents. At any realistic salary this dwarfs the per-page API cost. Reducing the review rate by improving the capture quality gate is almost always the cheapest cost lever available — and it improves conversion at the same time.

What to log, because you will be asked

An Indian regulator, a partner bank auditor or a court will eventually ask you to reconstruct a specific onboarding decision from two years ago. Design for that on day one; retrofitting it is several times the work.

JSON — the minimum audit record per onboarding attempt
{
  "attempt_id": "onb_01HXY...",
  "customer_ref": "cust_9f3a...",
  "timestamp": "2026-05-22T09:14:22Z",

  "capture": {
    "channel": "mobile_ios",
    "sdk_version": "3.4.1",
    "device_attestation": "passed",
    "quality_gate": {"result": "pass", "blur_score": 184.2, "retries": 1}
  },

  "extraction": {
    "document_type": "aadhaar",
    "engine": "paddleocr-2.9 + llm-structuring",
    "model_versions": {"ocr": "PP-OCRv4", "structuring": "<model-id>"},
    "fields": {"name": "...", "dob": "...", "id_number_masked": "XXXXXXXX1234"},
    "confidences": {"name": 0.97, "dob": 0.94, "id_number": 0.99},
    "grounding_check": "passed"
  },

  "validation": {"checksum": "pass", "cross_field_issues": []},

  "biometric": {
    "face_match_score": 0.91,
    "liveness": {"type": "passive", "result": "pass", "vendor": "...",
                 "vendor_ref": "lv_88121"}
  },

  "verification": {
    "source": "uidai_via_<licensed_provider>",
    "provider_ref": "txn_55219",
    "result": "match",
    "consent_ref": "cons_772a", "consent_timestamp": "2026-05-22T09:13:58Z"
  },

  "decision": {
    "action": "review",
    "reasons": ["face_borderline"],
    "policy_version": "kyc-policy-v7",
    "human_reviewer": "rev_114",
    "human_action": "approved",
    "human_rationale": "lighting artefact; document photo 9 years old",
    "final_timestamp": "2026-05-22T11:02:10Z"
  }
}

Three properties matter more than the exact schema. The record must be immutable — append-only, never updated in place. It must capture model and policy versions, because "which model made this decision" is the first question asked. And it must store the human rationale in free text, because that is what demonstrates a real review rather than a rubber stamp.

Watch out

Mask or tokenise identity numbers in logs. Storing full Aadhaar numbers in application logs is a data protection problem regardless of how well the rest of the system is built. Store the masked form plus a reference to the secured record.

Where this module ends and others begin

Identity verification proves who opened the account. It does not tell you whether they should be lent to, whether their transactions are suspicious, or whether they are on a sanctions list.

  • Sanctions and PEP screening — the third KYC question — is covered in the AML & Compliance module, because name matching across scripts and transliterations is its own substantial problem.
  • Mule and synthetic identity detection belongs to Fraud & Risk, since it surfaces in behaviour after onboarding rather than during it.
  • Model governance for the face-match and extraction models — bias testing, drift monitoring, documentation — is in Governance.
  • What you are permitted to collect and retain is in the India and global regulatory spine pages.
Watch out

Everything in this module is illustrative. Code shown here is correct in pattern and is not production-hardened. Any system performing KYC on real customers needs review by people qualified and accountable for compliance in your jurisdiction, and a security review of the capture path in particular.

Sources

Every figure, rule and date on this page, and where to check it. Entries are typed so you can see which are primary-sourced and which are industry reporting.

  1. officialRBI Master Direction on KYC — CDD requirements, V-CIP conditions, and record retention. www.rbi.org.in
  2. officialUIDAI — Aadhaar authentication routes and what an offline XML does and does not prove. uidai.gov.in
  3. officialDigiLocker — issued-document retrieval as an alternative to reading a photograph. www.digilocker.gov.in
  4. officialDPDP Act, 2023 — consent, purpose limitation and retention for identity data. www.meity.gov.in

Tooling, pricing and cost sources for this module are on its build sheet: sources →

Checked September 2026. Regulation in this area is actively developing; the date is part of the claim.

Ask an AI about this page

Opens your assistant with this page as the source, and a question rather than a summary. It will ask what you are building before it answers.

Nothing is sent from here. The link carries only this page’s title and address.