Verification
Proof of address
Capture an address document, classify it, check its freshness, and extract the applicant's address onto the identity.
The PROOF_OF_ADDRESS module captures a document that evidences where the applicant lives — a utility bill, bank statement, tax letter and similar — classifies it, checks it is recent enough, and extracts the address onto the applicant's identity. It runs in the hosted flow as a document-capture step and analyses the document's OCR text (not its identity structure), so it works on free-form letters and bills rather than templated ID documents.
Info
Proof of address is honest by design: it never auto-approves unreadable evidence. An unreadable capture is retryable; a readable-but-unverifiable one routes to manual review.
What it does
Capture and classify
The applicant photographs the document. The engine OCRs it and classifies the kind from keyword density (utility bill / bank statement / tax letter / lease / government letter / other).
Freshness check
The most recent plausible document date is parsed and compared against the configured max_age_days — a stale bill doesn't count as current proof.
Address extraction
A marker-bearing address block is extracted and persisted onto the identity (address_line1), so it flows through to your identity record and webhooks.
Accepted document kinds
Classification keys off keywords in the OCR text. Restrict which kinds count with the accepted_documents config; leave it unset to accept all of them:
document_kind | Recognised from (examples) |
|---|---|
utility_bill | Electricity, water, gas, internet/broadband, telecom bills (incl. Nepali NEA / KUKL issuers). |
bank_statement | Bank statements, account summaries, IBAN / balance / transaction text. |
tax_letter | Tax / revenue authority assessments (HMRC, IRS, Inland Revenue). |
lease | Tenancy / rental agreements, landlord–tenant documents. |
government_letter | Ministry, municipality, council or other official letters. |
other | No recognised kind — routes to review (unrecognised_document_kind). |
Configuration
max_age_daysnumberbodyMaximum age of the document date before it's considered stale (defaults to 90 days). A document older than this routes to review with document_too_old.
accepted_documentsarraybodyRestrict which document_kinds satisfy the step, e.g. ["utility_bill", "bank_statement"]. Anything outside the list classifies as other → review.
Nepali utility bills
Two Nepali utility bills are read natively via hosted OCR into structured fields:
| Bill | Issuer |
|---|---|
| NEA electricity bill | Nepal Electricity Authority (NEA) |
| KUKL water bill | Kathmandu Upatyaka Khanepani Limited (KUKL) |
A recognised bill is authoritative for the issuer and address, and its Bikram-Sambat bill date is converted to AD for the freshness check (a date the generic date parser can't read on its own). This enrichment is fail-soft: an unreachable service or an unrecognised document simply falls back to the generic document heuristics — nothing breaks.
Decisioning and outcomes
The status is derived from the classification, the parsed date and the extracted address — PROOF_OF_ADDRESS never silently approves:
| Outcome | When | Notes |
|---|---|---|
| Retry | OCR yielded almost nothing (bad capture). | Applicant is asked to retake; exhausted retries escalate to review (retries_exhausted). |
| Review ⚠️ | Readable but unverifiable — unrecognised kind, low classification confidence, no parseable date, a stale date, or no address found. | Held for an operator. |
| Approved ✅ | Recognised, accepted kind with a fresh document date and an extracted address. | Address is written to the identity. |
The result carries the classified document_kind, confidence, the parsed document_date and age_days, the configured max_age_days, the extracted address, and any warnings:
{
"feature": "PROOF_OF_ADDRESS",
"status": "approved",
"score": 80.0,
"warnings": [],
"data": {
"document_id": "b1f2…",
"engine": "poa",
"document_kind": "utility_bill",
"confidence": 0.8,
"document_date": "2026-06-02",
"age_days": 40,
"max_age_days": 90,
"address": "12 Ward, Baneshwor Marg, Kathmandu",
"warnings": []
}
}Read it from GET /v3/session/<id>/:
curl -s "$TF_BASE_URL/v3/session/$SESSION_ID/" \
-H "x-api-key: $TF_API_KEY" \
| jq '.results[] | select(.feature == "PROOF_OF_ADDRESS")'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 poa = session.results.find((r: any) => r.feature === "PROOF_OF_ADDRESS");
console.log(poa.status, poa.data.document_kind, poa.data.address);import requests
session = requests.get(
f"{BASE_URL}/v3/session/{session_id}/",
headers={"x-api-key": API_KEY},
timeout=15,
).json()
poa = next(r for r in session["results"] if r["feature"] == "PROOF_OF_ADDRESS")
print(poa["status"], poa["data"]["document_kind"], poa["data"]["address"])Warning reference
| Warning | Meaning |
|---|---|
unreadable_document | OCR yielded almost nothing — retryable. |
retries_exhausted | Retake budget spent; escalated to review. |
unrecognised_document_kind | Couldn't confidently classify the kind (or it isn't in accepted_documents). |
no_document_date | No plausible document date parsed. |
document_too_old | The document date exceeds max_age_days. |
no_address_found | No address block could be extracted. |
Note
DOCUMENT_AI is the sibling module for arbitrary custom documents — it extracts configured fields from any document type by regex/label proximity, where proof of address is specifically an address-evidence check with classification and freshness.
Common pitfalls
Warning
max_age_daysis measured against the document date, not the upload time. A bill issued 100 days ago is stale even if the applicant uploads it today; set the window to match your policy.- A screenshot or PDF export often OCRs poorly. Extremely low text yields a retry, not a decline — but repeated bad captures escalate to review, so guide applicants to photograph the physical document in good light.
otherisn't a silent pass. An unclassifiable or non-accepted kind routes to review; don't rely on it approving just because text was readable.- The extracted address overwrites
address_line1. It's a best-effort heuristic read — reconcile it against your own records before treating it as authoritative.