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_kindRecognised from (examples)
utility_billElectricity, water, gas, internet/broadband, telecom bills (incl. Nepali NEA / KUKL issuers).
bank_statementBank statements, account summaries, IBAN / balance / transaction text.
tax_letterTax / revenue authority assessments (HMRC, IRS, Inland Revenue).
leaseTenancy / rental agreements, landlord–tenant documents.
government_letterMinistry, municipality, council or other official letters.
otherNo recognised kind — routes to review (unrecognised_document_kind).

Configuration

max_age_daysnumberbody

Maximum 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_documentsarraybody

Restrict 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:

BillIssuer
NEA electricity billNepal Electricity Authority (NEA)
KUKL water billKathmandu 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:

OutcomeWhenNotes
RetryOCR 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.
ApprovedRecognised, 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

WarningMeaning
unreadable_documentOCR yielded almost nothing — retryable.
retries_exhaustedRetake budget spent; escalated to review.
unrecognised_document_kindCouldn't confidently classify the kind (or it isn't in accepted_documents).
no_document_dateNo plausible document date parsed.
document_too_oldThe document date exceeds max_age_days.
no_address_foundNo 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

  1. max_age_days is 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.
  2. 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.
  3. other isn't a silent pass. An unclassifiable or non-accepted kind routes to review; don't rely on it approving just because text was readable.
  4. The extracted address overwrites address_line1. It's a best-effort heuristic read — reconcile it against your own records before treating it as authoritative.

FAQ