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 →
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 →
Every tool for this module, how to use each one, what it costs, the best combinations and three recommended builds: Identity Build Sheet →
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 →
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:
- Does this person exist? Is the identity real, and does the document refer to a real registered person?
- Is this person who they claim to be? Is the human in front of the camera the same human on the document?
- 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.
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
| Task | How well AI does it |
|---|---|
| Read printed text from a clear document | Very well. Effectively solved. |
| Read a document at an angle, in poor light | Well, with preprocessing. The preprocessing matters as much as the model. |
| Find the right field on an unfamiliar layout | Reasonably. Vision-language models are good at this; older OCR is not. |
| Read handwriting | Inconsistently. Better than five years ago, still unreliable for anything consequential. |
| Match a selfie to a document photo | Very well, when both images are decent. |
| Tell a live face from a photograph held to the camera | Well, against casual attempts. Against a determined attacker with a deepfake, this is an arms race. |
| Decide whether a document is genuine | Partially. It catches crude forgeries. It cannot confirm a document is real — only the issuing database can. |
| Decide whether to onboard someone | It should not. That is a business and regulatory decision with a documented reason. |
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.
The pipeline, stage by stage
Nine stages. Most teams build three of them, then spend months debugging the six they skipped.
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 trailThe 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.
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
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:
| Engine | Type | Strongest at | Breaks on | Licence note |
|---|---|---|---|---|
| Tesseract 5 | Classic | Clean printed text, tiny footprint, fast | Angles, low contrast, complex layout, most Indic scripts | Apache 2.0 |
| PaddleOCR | Deep learning | Complex layouts, tables, multilingual; PP-Structure adds layout analysis | Heavy dependency; environment/model-loading friction | Apache 2.0 |
| docTR | Deep learning | Good detection+recognition in one pipeline, tunable speed/accuracy | Less multilingual coverage than Paddle | Apache 2.0 |
| EasyOCR | Deep learning | Fastest to a working prototype, 80+ languages | Roughly 3x slower inference; lower accuracy ceiling at volume | Apache 2.0 |
| Surya | VLM-style | Best layout analysis in the open group, 90+ languages, dense pages | GPU cost | Check 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-OCR | Vision-language | Reads contextually, infers structure, handles messy layouts and some handwriting | GPU-hungry; can hallucinate plausible text that was never on the page | Varies by model |
| Cloud document APIs Google Document AI, AWS Textract, Azure Document Intelligence, Mistral OCR | Managed | No infrastructure, good accuracy, pre-built ID parsers | Per-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.
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.
| Approach | How it works | Use when | Weakness |
|---|---|---|---|
| Regex + anchors | Find a label, take the text near it | One fixed layout you control | Breaks on any layout change; brittle across document versions |
| Positional templates | Fields live at known coordinates after alignment | Standardised cards, high volume, same issuer | Requires a template per document type and per version |
| Layout models (LayoutLM family, Donut) | Model learns which text span is which field from position + content | Many layouts, enough labelled examples to fine-tune | Needs training data; a real ML project |
| VLM prompting | Send the image, ask for JSON | Unknown or varied layouts, fast to build | Hallucination; cost per page; must validate every field |
| Hybrid | OCR extracts text, an LLM structures it, validators check it | Most production systems | More moving parts to monitor |
The hybrid pattern, which is what most teams end up with
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, NoneThe 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.
# 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
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 issuesFace 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.
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 2026India — identity verification and KYC
Verified May 2026Face matching and liveness
Verified May 2026Pricing 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
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.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.
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 dHow to actually set the numbers
Do not pick thresholds from a vendor benchmark. Derive them from your own review queue:
- Launch deliberately conservative. Route far more to review than you think necessary.
- Have reviewers record, for every item, whether the machine was right.
- 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%.
- Loosen one field at a time and watch the downstream fraud rate, not just the review volume.
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.
| Attack | What it looks like | What counters it |
|---|---|---|
| Print attack | Photograph of a photograph held to the camera | Passive liveness — moiré, texture, reflection; easily caught |
| Replay attack | A recorded video played to the camera | Active liveness with randomised challenges |
| Deepfake / face swap | Synthetic face rendered live over a real feed | Vendor-side deepfake detection; treat as an arms race, not a fix |
| Camera injection | Virtual camera feeds a prepared stream, bypassing capture entirely | Device attestation, SDK integrity checks, refusing virtual camera devices — this is the one most teams miss |
| Document tampering | Edited photo, altered DOB, substituted portrait | Tamper detection, font/spacing analysis, and crucially the issuing-database lookup |
| Synthetic identity | Real ID number, fabricated person assembled around it | Cross-source consistency; bureau and CKYCR history checks |
| Coerced / mule onboarding | A genuine person, genuinely live, opening an account for someone else | No liveness system catches this. Behavioural and transaction monitoring do — see the Fraud module. |
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.
| Approach | Cost driver | Sensible at |
|---|---|---|
| Managed cloud document API | Per page, tiered down with volume | Low to moderate volume; no ML team; no data-residency blocker |
| Self-hosted classic OCR (Tesseract, Paddle) | CPU only. Effectively free per page after infrastructure | High volume, clean documents, strict data residency |
| Self-hosted VLM OCR | GPU hours — the dominant line item | Messy 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 match | Almost everyone, because Aadhaar access is indirect anyway |
| Human review | Reviewer minutes | This is usually the largest line and the one nobody models |
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.
{
"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.
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.
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.
- officialRBI Master Direction on KYC — CDD requirements, V-CIP conditions, and record retention. www.rbi.org.in
- officialUIDAI — Aadhaar authentication routes and what an offline XML does and does not prove. uidai.gov.in
- officialDigiLocker — issued-document retrieval as an alternative to reading a photograph. www.digilocker.gov.in
- 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.