mcpField
D
didit-proof-of-address

didit-proof-of-address

github.com/didit-protocolSecurity29196 installsUpdated 2w ago
Repository

About didit-proof-of-address

A Didit API skill for verifying proof-of-address documents, intended for applications that need to validate a person's address. It extracts document details, checks authenticity signals, and can compare extracted values with expected data.

Key features

  1. 01Classify address documents and extract details
  2. 02Parse and geocode addresses
  3. 03Check dates, names, addresses, and countries
  4. 04Inspect PDF and EXIF tampering signals
  5. 05Detect QR codes and barcodes

Use cases

  1. 01Validate address documents during onboarding
  2. 02Compare a submitted document with expected identity details
  3. 03Review document age and possible manipulation

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/didit-protocol/skills --skill didit-proof-of-address

After installing, type/didit-proof-of-addressin Claude Code to confirm it loaded.

Bundled filesscripts/verify_address.py

Details

From the project README

Didit Proof of Address API

Overview

Verifies an address document (image or PDF) in one call. The document is classified (type and subtype), the issuer, dates, holder name and address are extracted, the address is parsed and geocoded, PDF/EXIF forensics look for tampering, and optional expected_* values are cross-checked. The answer is Approved or Declined with a warnings list explaining every issue found.

Pricing: $0.20 per call (every 200 response is billed, Approved or Declined; standalone calls have no free tier). These are the list prices until 1 November 2026, when Didit pricing changes and the 500 free checks a month become $10 of free credit a month. Check didit.me/pricing for current prices.

Key constraints:

  • Formats: JPG, JPEG, PNG, TIFF, PDF, WEBP. Multi-page PDFs are supported. Encrypted PDFs need document_password.
  • Maximum upload size: 50 MB for a PDF, 6 MB for an image (larger files return 400)
  • Latency: typically 5-15 s, multi-page PDFs up to ~30 s. Use a client timeout of at least 45 s and do not retry before the call completes.
  • Maximum document age, by type (defaults): 3 months for utility bills and bank statements, 12 months for government-issued and other documents. A month counts as 30 days. Change it per request with poa_document_age_months (1-120, or -1 for no age check). A printed expiry date that has passed also expires the document.
  • Capture guidance: full color, all corners and all pages visible, no digital editing.

Capabilities: document classification, LLM-based extraction, address parsing and geocoding (latitude/longitude), language detection, issue-date and age checks, name/address/country cross-checks, PDF and EXIF tamper forensics, bank detail extraction, QR code and barcode detection.

Accepted document types (document_type):

TypeExamples (document_subtype)
UTILITY_BILLElectricity, water, gas, internet, phone, combined utilities bill
BANK_STATEMENTAccount statement, credit card statement, mortgage or loan statement
GOVERNMENT_ISSUED_DOCUMENTTax assessment, residency certificate, voter registration card
OTHER_POA_DOCUMENTLease or rental agreement, insurance policy, employer letter

A document that cannot be classified gets document_type: "UNKNOWN" and is declined with INVALID_DOCUMENT_TYPE.

API Reference: https://docs.didit.me/standalone-apis/proof-of-address Feature Guide: https://docs.didit.me/core-technology/proof-of-address/overview Warnings: https://docs.didit.me/core-technology/proof-of-address/warnings-proof-of-address


Authentication

Every request needs the x-api-key header. Use your application's API key from the Didit Business Console under Developers > API keys, or create an account and key programmatically (see Getting Started). Named keys with custom access need Sessions write to run standalone checks; a key limited to some workflows or to approved sessions cannot run them.

Getting Started (No Account Yet?)

If you don't have a Didit API key, create one in 2 API calls:

  1. Register: POST https://apx.didit.me/auth/v2/programmatic/register/ with {"email": "[email protected]", "password": "MyStr0ng!Pass"}
  2. Check email for a 6-character verification code
  3. Verify: POST https://apx.didit.me/auth/v2/programmatic/verify-email/ with {"email": "[email protected]", "code": "A3K9F2"}. The key is in the response at application.api_key.

To add credits: GET /v3/billing/balance/ to check, POST /v3/billing/top-up/ with {"amount_in_dollars": 50} for a Stripe checkout link.

See the didit-verification-management skill for full platform management (workflows, sessions, users, billing).


Endpoint

POST https://verification.didit.me/v3/poa/
Headers
HeaderValueRequired
x-api-keyYour API keyYes
Content-Typemultipart/form-dataYes
Request Parameters (multipart/form-data)
ParameterTypeRequiredDefaultDescription
documentfileYes-The PoA document: tiff, jpg, jpeg, png, pdf or webp. PDF max 50 MB, image max 6 MB
document_passwordstringNo-Password of an encrypted PDF. Missing or wrong password returns 400
expected_addressstringNo-Address to check. It is geocoded and compared with the document address; a mismatch, or an expected address that cannot be verified, adds ADDRESS_MISMATCH_WITH_PROVIDED
expected_countrystringNo-ISO 3166-1 alpha-2 or alpha-3 code (ES or ESP). A different document country adds POA_COUNTRY_MISMATCH_WITH_PROVIDED
expected_first_namestringNo-First name to check against name_on_document (fuzzy, transliteration-aware). Send it with expected_last_name: the name check runs only when both are set
expected_last_namestringNo-Last name for the same check. A score below 86 (0-100) adds NAME_MISMATCH_WITH_PROVIDED
poa_languages_allowedstringNoall 51 supported languagesComma-separated ISO 639-1 codes, e.g. en,es. Another language adds UNSUPPORTED_DOCUMENT_LANGUAGE; an unknown code returns 400 with the supported list
poa_document_age_monthsstringNoutility_bill:3,bank_statement:3,government_issued_document:12,other_poa_document:12Maximum age per type as key:value pairs. Values 1-120, or -1 for no age check. When you send it, list every type you accept: omitted types count as not accepted (informational POA_DOCUMENT_NOT_SUPPORTED_FOR_APPLICATION) and get no age check
poa_address_mismatch_actionDECLINE / NO_ACTIONNoDECLINEApplies to ADDRESS_MISMATCH_WITH_PROVIDED, NAME_MISMATCH_WITH_PROVIDED and POA_COUNTRY_MISMATCH_WITH_PROVIDED
poa_document_issues_actionDECLINE / NO_ACTIONNoDECLINEApplies to DOCUMENT_METADATA_MISMATCH
poa_document_authenticity_actionDECLINE / NO_ACTIONNoDECLINEApplies to SUSPECTED_DOCUMENT_MANIPULATION
poa_unsupported_language_actionDECLINE / NO_ACTIONNoDECLINEApplies to UNSUPPORTED_DOCUMENT_LANGUAGE
poa_issuer_not_identified_actionDECLINE / NO_ACTIONNoDECLINEApplies to ISSUER_NOT_IDENTIFIED
poa_name_mismatch_actionDECLINE / NO_ACTIONNoDECLINEAccepted but not applied: name mismatches follow poa_address_mismatch_action
save_api_requestbooleanNotruetrue: stored as a session (Business Console, GET /v3/session/{sessionId}/decision/ with the returned request_id, status.updated webhook). false: nothing is stored and request_id cannot be looked up
vendor_datastringNo-Your identifier for the user, echoed back and stored on the session
metadataJSON stringNo-JSON object sent as a string field, e.g. {"flow":"onboarding"}. Echoed back and stored

Sandbox: sandbox API keys skip processing and billing. After request validation (bad input still returns 400) they get a fixed Approved mock response and nothing is stored.

Example
import requests

with open("bill.pdf", "rb") as f:
    response = requests.post(
        "https://verification.didit.me/v3/poa/",
        headers={"x-api-key": "YOUR_API_KEY"},
        files={"document": ("bill.pdf", f, "application/pdf")},
        data={
            "expected_first_name": "Jane",
            "expected_last_name": "Doe",
            "expected_country": "ESP",
            "poa_languages_allowed": "en,es",
            "vendor_data": "user-123",
        },
        timeout=60,  # extraction takes 5-15 s, up to ~30 s for multi-page PDFs
    )
response.raise_for_status()
poa = response.json()["poa"]
print(poa["status"], poa["poa_formatted_address"], [w["risk"] for w in poa["warnings"]])
const formData = new FormData();
formData.append("document", documentFile); // File or Blob
formData.append("expected_first_name", "Jane");
formData.append("expected_last_name", "Doe");
formData.append("expected_country", "ESP");
formData.append("vendor_data", "user-123");

const response = await fetch("https://verification.didit.me/v3/poa/", {
  method: "POST",
  headers: { "x-api-key": "YOUR_API_KEY" },
  body: formData,
  signal: AbortSignal.timeout(60_000),
});
if (!response.ok) throw new Error(`PoA failed: ${response.status} ${await response.text()}`);
const { poa } = await response.json();
console.log(poa.status, poa.warnings.map((w: { risk: string }) => w.risk));
Response (200 OK)
{
  "request_id": "4b523a8d-ed32-45e4-8aa8-a89eda459d88",
  "poa": {
    "issuing_state": "ESP",
    "document_type": "UTILITY_BILL",
    "document_subtype": "ELECTRICITY_BILL",
    "document_language": "es",
    "issuer": "Acme Energy",
    "issue_date": "2026-09-15",
    "expiration_date": null,
    "poa_address": "C/ Mayor 10, 2B, 28013 Madrid",
    "poa_formatted_address": "Calle Mayor 10, 2B, 28013 Madrid, Spain",
    "poa_parsed_address": {
      "street_1": "Calle Mayor 10",
      "street_2": "2B",
      "city": "Madrid",
      "region": "Comunidad de Madrid",
      "country": "ES",
      "postal_code": "28013",
      "document_location": {"latitude": 40.4155, "longitude": -3.7074}
    },
    "expected_details_address": null,
    "expected_details_formatted_address": null,
    "expected_details_parsed_address": null,
    "name_on_document": "JANE DOE",
    "extra_fields": {
      "bank_account_number": null,
      "bank_iban": null,
      "bank_sort_code": null,
      "bank_routing_number": null,
      "bank_swift_bic": null,
      "bank_branch_name": null,
      "bank_branch_address": null,
      "document_phone_number": null,
      "additional_names": []
    },
    "detected_codes": [],
    "status": "Approved",
    "warnings": []
  },
  "vendor_data": "user-123",
  "metadata": null,
  "created_at": "2026-10-12T09:30:00.000000+00:00"
}

A declined document still returns 200. Example warning for a bill older than the allowed age:

{
  "risk": "POA_DOCUMENT_EXPIRED",
  "feature": "PROOF_OF_ADDRESS",
  "additional_data": {
    "expiration_reason": "age_window",
    "max_age_months": 3,
    "document_subtype": "ELECTRICITY_BILL",
    "document_type": "UTILITY_BILL",
    "issue_date": "2026-02-10"
  },
  "log_type": "error",
  "short_description": "Document expired",
  "long_description": "The submitted document is older than the maximum accepted age of 3 months from its issue date, which exceeds the acceptable time period for validity."
}

The response does not include the document file or the forensic metadata. With save_api_request=true, GET /v3/session/{request_id}/decision/ returns them in poa_verifications[] (document_file is a temporary URL that expires after about 4 hours).

Status Values & Handling
StatusMeaningAction
"Approved"No warning resolved to a declineUse the parsed address; information warnings may still be present
"Declined"At least one warning has log_type: "error"Read warnings[].risk and ask for another document or fix the expected_* input

This endpoint never returns In Review or Not Finished. Always read poa.status and poa.warnings, not just the HTTP code.

Error Responses
CodeMeaningAction
400Invalid requestRead the field errors in the response body
401The API key has expiredRotate it or create a new key in Developers > API keys
403Missing, wrong or revoked API key; a scoped key without access to this endpoint; a request from outside the key's IP allowlist; or not enough creditsRead the message in detail or error: fix the x-api-key header or the key's access, or top up
429Rate limit: 300 POST/PATCH/DELETE and 600 GET requests a minute per keyWait the Retry-After seconds, then retry

400 causes on this endpoint: no document, an extension outside the allowed list, a PDF over 50 MB or an image over 6 MB, an encrypted PDF without (or with a wrong) document_password, an unknown code in poa_languages_allowed, a bad poa_document_age_months pair, or a document the extraction cannot process ({"error": ["Error extracting POA information"]}). The credit check runs before any processing; a not-enough-credits 403 body is {"error": "You don't have enough credits to perform this request. ..."}.


Response Field Reference

FieldTypeDescription
request_iduuidSession id when save_api_request=true; otherwise a one-off correlation id
poa.statusstring"Approved" or "Declined"
poa.issuing_statestring/nullCountry of the document, usually ISO 3166-1 alpha-3 (ESP)
poa.document_typestringUTILITY_BILL, BANK_STATEMENT, GOVERNMENT_ISSUED_DOCUMENT, OTHER_POA_DOCUMENT or UNKNOWN
poa.document_subtypestringe.g. ELECTRICITY_BILL, WATER_BILL, ACCOUNT_STATEMENT, CREDIT_CARD_STATEMENT, TAX_ASSESSMENT; UNKNOWN when not determined
poa.document_languagestring/nullISO 639-1 code
poa.issuerstring/nullIssuing company or organization; null adds ISSUER_NOT_IDENTIFIED
poa.issue_datedate/nullYYYY-MM-DD; null declines with UNABLE_TO_EXTRACT_ISSUE_DATE
poa.expiration_datedate/nullExpiry date printed on the document, if any
poa.poa_addressstring/nullAddress as printed; null declines with MISSING_ADDRESS_INFORMATION
poa.poa_formatted_addressstring/nullGeocoded, normalized address
poa.poa_parsed_addressobject/null{street_1, street_2, city, region, country, postal_code, document_location}; country is ISO alpha-2
poa.poa_parsed_address.document_locationobject/null{latitude, longitude} of the document address
poa.expected_details_addressstring/nullEcho of expected_address
poa.expected_details_formatted_addressstring/nullGeocoded expected_address
poa.expected_details_parsed_addressobject/nullGeocoding result for expected_address, including an is_verified flag
poa.name_on_documentstring/nullPrimary holder name; null declines with POA_NAME_NOT_DETECTED
poa.extra_fieldsobjectbank_account_number, bank_iban, bank_sort_code, bank_routing_number, bank_swift_bic, bank_branch_name, bank_branch_address, document_phone_number (null when absent), additional_names (array)
poa.detected_codesarrayQR codes and barcodes: type, page_number, position, x, y, width, height, page_width, page_height, raw_payload (base64), parsed_payload (null when undecodable)
poa.warningsarray{risk, feature, additional_data, log_type, short_description, long_description}; feature is always PROOF_OF_ADDRESS
vendor_datastring/nullEcho of your vendor_data
metadataobject/nullEcho of your metadata
created_atstringISO 8601 UTC timestamp

Warning Tags

log_type is error (sets Declined) or information (no effect). warning is never produced on this endpoint.

Always decline
TagCause
MISSING_ADDRESS_INFORMATIONNo address could be extracted
POA_DOCUMENT_EXPIREDOlder than the allowed age for its type, or a printed expiry date has passed. additional_data: expiration_reason (age_window or printed_expiration), document_type, document_subtype, issue_date, plus max_age_months or expiration_date
INVALID_DOCUMENT_TYPENot classifiable as a PoA document (document_type: "UNKNOWN")
UNABLE_TO_VALIDATE_DOCUMENT_AGEThe age check itself failed
UNABLE_TO_EXTRACT_ISSUE_DATENo issue date found (not configurable on this endpoint)
POA_NAME_NOT_DETECTEDNo holder name found (not configurable on this endpoint)
Configurable (DECLINE by default, NO_ACTION makes them information)
TagCauseOption
NAME_MISMATCH_WITH_PROVIDEDname_on_document does not match expected_first_name + expected_last_namepoa_address_mismatch_action
ADDRESS_MISMATCH_WITH_PROVIDEDDocument address differs from expected_address, or either address could not be verifiedpoa_address_mismatch_action
POA_COUNTRY_MISMATCH_WITH_PROVIDEDDocument country differs from expected_country. additional_data: expected_country, extracted_countrypoa_address_mismatch_action
DOCUMENT_METADATA_MISMATCHEmpty file, unreadable file metadata, or malformed EXIF datespoa_document_issues_action
SUSPECTED_DOCUMENT_MANIPULATIONForensics found edits: modified after digital signing, overlay-text manipulation, a known PDF editor, inconsistent EXIF dates or a suspicious re-export. additional_data: reason, detection_methodpoa_document_authenticity_action
UNSUPPORTED_DOCUMENT_LANGUAGELanguage not in poa_languages_allowedpoa_unsupported_language_action
ISSUER_NOT_IDENTIFIEDNo issuing organization foundpoa_issuer_not_identified_action
Informational on this endpoint (never decline)
TagCause
POA_DOCUMENT_NOT_SUPPORTED_FOR_APPLICATIONThe detected type was left out of poa_document_age_months
UNPARSABLE_OR_INVALID_ADDRESSAn address was read but could not be parsed and geocoded into a valid residential address

FUTURE_ISSUE_DATE, POOR_DOCUMENT_QUALITY, NAME_MISMATCH_ID_VERIFICATION, POA_NAME_MISMATCH_BETWEEN_DOCUMENTS and POA_MAX_ATTEMPTS_EXCEEDED are never produced by this endpoint (they belong to workflow sessions). A future-dated document is not auto-declined here.


Common Workflows

Basic Address Verification
1. POST /v3/poa/ with the document (client timeout >= 45 s)
2. "Approved" -> store poa_formatted_address / poa_parsed_address
   "Declined" -> branch on warnings[].risk:
     POA_DOCUMENT_EXPIRED            -> ask for a more recent document
     MISSING_ADDRESS_INFORMATION     -> ask for a clearer, complete document
     INVALID_DOCUMENT_TYPE           -> ask for a utility bill, bank statement or official letter
     UNABLE_TO_EXTRACT_ISSUE_DATE    -> ask for a document with a visible issue date
     SUSPECTED_DOCUMENT_MANIPULATION -> do not accept; ask for the original file
Address Plus Identity Cross-Check

Standalone calls are not linked to an ID check, so pass the data you already trust:

1. POST /v3/id-verification/ -> read id_verification.first_name, last_name, issuing_state
   (see the didit-id-document-verification skill)
2. POST /v3/poa/ with expected_first_name, expected_last_name, expected_country
   (and expected_address if the user typed one)
3. NAME_MISMATCH_WITH_PROVIDED / ADDRESS_MISMATCH_WITH_PROVIDED / POA_COUNTRY_MISMATCH_WITH_PROVIDED
   -> Declined by default; send poa_address_mismatch_action=NO_ACTION to only flag them
Retrieve or Correct a Stored Result
1. Call with save_api_request=true (default); keep request_id
2. GET /v3/session/{request_id}/decision/ -> poa_verifications[] with document_file and forensics
3. To fix extracted fields after a manual review, PATCH /v3/session/{sessionId}/update-poa-data/
   (documented in the didit-verification-management skill)

Utility Scripts

verify_address.py: Verify a proof of address document from the command line. Prints the JSON response, then the status, address and warning risks.

# Requires: pip install requests
export DIDIT_API_KEY="your_api_key"
python scripts/verify_address.py utility_bill.pdf
python scripts/verify_address.py bank_statement.jpg --vendor-data user-123
python scripts/verify_address.py bill.pdf --first-name Jane --last-name Doe --country ESP
python scripts/verify_address.py bill.pdf --expected-address "Calle Mayor 10, 28013 Madrid" --languages en,es
python scripts/verify_address.py bill.pdf --document-age utility_bill:6,bank_statement:6,government_issued_document:12,other_poa_document:12
python scripts/verify_address.py locked.pdf --password "pdf-password" --no-save

Comments

Markdown supported