openapi: 3.0.0 info: version: 3.0.0 title: Didit Verification Billing Standalone APIs API description: Identity verification API. Authenticate with x-api-key header. servers: - url: https://verification.didit.me tags: - name: Standalone APIs paths: /v3/id-verification/: post: summary: ID Verification (document OCR + fraud checks) description: 'OCR an identity document and run document fraud checks in one call — returns the extracted holder fields, the parsed MRZ, an `Approved`/`Declined` `status`, and a `warnings` list explaining every issue found. Supports 14,000+ document types from 220+ countries and territories. **How it works.** Send the document''s `front_image` (required) and, for two-sided documents, the `back_image`. Upload uncropped photos with all four corners of the document visible — the service detects, aligns, and crops the document itself, so do not pre-crop. The document is classified automatically (you never declare the country or document type), the visual zone is read with OCR, and the MRZ and any barcodes are decoded. Cross-checks then validate dates, number formats, and MRZ check digits, and compare the visual zone against the MRZ. With `perform_document_liveness=true`, the images are additionally screened for screen replays, printed copies, and portrait manipulation. **Decision logic.** `status` is `Approved` unless at least one warning resolves to a decline. Fraud and hard-failure risks always decline: `DOCUMENT_EXPIRED`, `SCREEN_CAPTURE_DETECTED`, `PRINTED_COPY_DETECTED`, `PORTRAIT_MANIPULATION_DETECTED`, plus extraction failures (`NAME_NOT_DETECTED`, `DATE_OF_BIRTH_NOT_DETECTED`, `DOCUMENT_NUMBER_NOT_DETECTED`). (`PORTRAIT_IMAGE_NOT_DETECTED`, `COULD_NOT_DETECT_DOCUMENT_TYPE`, `INVALID_DATE`, and `MRZ_NOT_DETECTED` exist in workflow sessions but are never produced by this standalone endpoint.) Three risk groups are configurable per request via `invalid_mrz_action`, `inconsistent_data_action`, and `expiration_date_not_detected_action` (`DECLINE` or `NO_ACTION`). All other warnings (e.g. `POSSIBLE_DUPLICATED_USER`) are informational and never decline on their own. A document that cannot be processed at all returns `400` with `{"error": "COULD_NOT_RECOGNIZE_DOCUMENT"}`; a readable but problematic document returns `200` with `status: "Declined"` — always inspect `id_verification.status` and `id_verification.warnings`, not just the HTTP code. **Billing.** Each `200` response consumes one ID Verification API credit (standalone APIs have no free tier). When the organization''s balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any image processing. **Session persistence (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session: it appears in the Business Console, the returned `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/`, the cropped document/portrait images are stored and returned as short-lived media URLs (`https:///ocr/...`), and a `status.updated` webhook is emitted to your configured webhook endpoints. When `false`, nothing is stored, `request_id` is a one-off correlation UUID, and `portrait_image`/`front_image`/`back_image` are returned inline as base64-encoded JPEG strings. **Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload (a fictional "Sandbox User" USA identity card), no session is persisted, and no credits are consumed. **Authentication.** Send your application''s API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`) — this API never returns `401`. **Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.' operationId: post_v3id-verification tags: - Standalone APIs parameters: [] requestBody: required: true content: multipart/form-data: schema: type: object required: - front_image properties: front_image: type: string format: binary description: 'Front side of the identity document. Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `webp`, `pdf`. Maximum upload size: **10 MB** (larger files are rejected with `400`). PDFs are rendered to an image before OCR; encrypted PDFs require `front_image_password`. Images are automatically compressed to ~0.5 MB and EXIF orientation is applied. Upload the full, uncropped capture with all four corners of the document visible.' front_image_password: type: string description: Password to decrypt `front_image` when it is an encrypted PDF. Sending an encrypted PDF without (or with a wrong) password returns `400`. writeOnly: true back_image: type: string format: binary description: Back side of the document — send it whenever the document has one (many ID cards carry the MRZ or a barcode on the back; omitting it simply leaves the MRZ and back-side fields empty in the response — this endpoint does not raise a missing-MRZ warning). Same format and size limits as `front_image`. When omitted, `back_image` and back-side fields are `null` in the response. back_image_password: type: string description: Password to decrypt `back_image` when it is an encrypted PDF. writeOnly: true perform_document_liveness: type: boolean default: false description: 'When `true`, also screens both images for presentation fraud: screen replays (`SCREEN_CAPTURE_DETECTED`), printed copies (`PRINTED_COPY_DETECTED`), and portrait manipulation (`PORTRAIT_MANIPULATION_DETECTED`). Any of these auto-declines. Adds latency, so enable it only when you need fraud screening.' example: true minimum_age: type: integer nullable: true default: null minimum: 1 maximum: 120 description: Accepted and validated (1–120) but **currently not applied** by this endpoint — it never produces a `MINIMUM_AGE_NOT_MET` warning from this field. Enforce age rules from the returned `date_of_birth`/`age`, or use a verification-session workflow with age restrictions. expiration_date_not_detected_action: type: string enum: - NO_ACTION - DECLINE default: NO_ACTION description: 'What to do when no expiration date can be read (`EXPIRATION_DATE_NOT_DETECTED`). Default `NO_ACTION` because many identity documents print no expiry date. Note: a *detected and past* expiry date always declines (`DOCUMENT_EXPIRED`), regardless of this option.' invalid_mrz_action: type: string enum: - NO_ACTION - DECLINE default: DECLINE description: What to do when the extracted MRZ fails check-digit validation (`MRZ_VALIDATION_FAILED`). A missing MRZ raises no warning on this endpoint. Only relevant for documents that carry an MRZ. `REVIEW` is not supported on this endpoint and returns `400`. inconsistent_data_action: type: string enum: - NO_ACTION - DECLINE default: DECLINE description: 'What to do when extracted data is internally inconsistent: `DATA_INCONSISTENT`, `MRZ_AND_DATA_EXTRACTED_FROM_OCR_NOT_SAME` (visual zone disagrees with the MRZ), or `DOCUMENT_NAME_DIFFERENT_FROM_OTHER_APPROVED_DOCUMENTS`.' preferred_characters: type: string enum: - latin - non_latin default: latin description: Preferred script for name/address fields on documents that print both Latin and non-Latin text (Arabic, Cyrillic, CJK, …). `latin` returns transliterated/Latin values; `non_latin` prefers the native script. save_api_request: type: boolean default: true description: When `true` (default), persists the call as an API-type session — visible in the Business Console, retrievable via `GET /v3/session/{sessionId}/decision/` using the returned `request_id`, announced through a `status.updated` webhook, and with document images returned as media URLs. When `false`, nothing is stored, `request_id` is a transient UUID, and images are returned inline as base64 JPEG strings. example: true vendor_data: type: string description: Optional opaque string (your internal user id, email, UUID…) stored on the persisted session and echoed back in the response. Use it to correlate API calls with your own records and to filter sessions later. example: user-123 metadata: type: object additionalProperties: true description: Optional JSON object stored with the session (when `save_api_request=true`) and echoed back in the response. In multipart requests, send it as a JSON-encoded string field (e.g. `metadata={"flow":"onboarding"}`) — it is parsed into an object. example: flow: onboarding example: front_image: (binary JPEG/PNG/PDF of the document front) back_image: (binary JPEG/PNG/PDF of the document back) perform_document_liveness: true save_api_request: true vendor_data: user-123 metadata: flow: onboarding responses: '200': description: 'Document processed. `id_verification.status` is `Approved` or `Declined`; every detected issue is itemized in `id_verification.warnings`. A problematic document still returns `200` with `status: "Declined"` — inspect the body, not just the HTTP code. When `save_api_request=true`, `request_id` is the persisted session id and image fields are short-lived media URLs; with `save_api_request=false` they are inline base64 JPEG strings.' content: application/json: examples: Approved: summary: Two-sided ID card read cleanly (saved — images as media URLs) value: request_id: 11d219ed-d59c-4b1d-8d65-9b933f12d5b8 id_verification: status: Approved document_type: Identity Card document_subtype: ID_CARD_GENERIC document_number: CBX164224 personal_number: 20446581H portrait_image: https:///ocr/11d219ed-d59c-4b1d-8d65-9b933f12d5b8-portrait_image-8c2f.jpg?signature=... front_image: https:///ocr/11d219ed-d59c-4b1d-8d65-9b933f12d5b8-front_image-4a1e.jpg?signature=... back_image: https:///ocr/11d219ed-d59c-4b1d-8d65-9b933f12d5b8-back_image-77b0.jpg?signature=... front_image_camera_front: null back_image_camera_front: null front_image_camera_front_face_match_score: null back_image_camera_front_face_match_score: null front_image_quality_score: focus_score: 78 brightness_score: 83.2 brightness_issue: ok is_document_fully_visible: true resolution_score: 40.7 overall_score: 70.2 back_image_quality_score: focus_score: 100 brightness_score: 95.6 brightness_issue: ok is_document_fully_visible: true resolution_score: 43 overall_score: 84.4 date_of_birth: '1980-01-13' age: 46 expiration_date: '2032-03-31' date_of_issue: '2022-03-31' issuing_state: ESP issuing_state_name: Spain first_name: Julio Francisco last_name: Fores Sena full_name: Julio Francisco Fores Sena gender: M address: Brda. Urb. El Cardonal 0052 52 Po3 B,Taco,San Cristobal De La Laguna,Santa Cruz De Tenerife formatted_address: Av. el Cardonal, 52, b, 38108 La Laguna, Santa Cruz de Tenerife, Spain place_of_birth: Valencia, Valencia marital_status: UNKNOWN nationality: ESP extra_fields: first_surname: Fores second_surname: Sena mrz: surname: FORES SENA name: JULIO FRANCISCO country: ESP nationality: ESP birth_date: '800113' expiry_date: '320331' sex: M document_type: ID document_number: CBX164224 optional_data: 20446581H optional_data_2: '' birth_date_hash: '9' expiry_date_hash: '8' document_number_hash: '3' final_hash: '3' personal_number: 20446581H warnings: [] errors: [] mrz_type: TD1 mrz_string: 'IDESPCBX164224320446581H<<<<<< 8001139M3203318ESP<<<<<<<<<<<3 FORES/ocr/f61681b8-9422-4378-ac3e-04c6a247fb45-portrait_image-1d9a.jpg?signature=... front_image: https:///ocr/f61681b8-9422-4378-ac3e-04c6a247fb45-front_image-5c2b.jpg?signature=... back_image: null front_image_camera_front: null back_image_camera_front: null front_image_camera_front_face_match_score: null back_image_camera_front_face_match_score: null front_image_quality_score: focus_score: 100 brightness_score: 95.7 brightness_issue: ok is_document_fully_visible: true resolution_score: 22.2 overall_score: 79.3 back_image_quality_score: null date_of_birth: '1997-01-30' age: 29 expiration_date: '2025-11-26' date_of_issue: null issuing_state: ESP issuing_state_name: Spain first_name: Lucia last_name: Marin Castro full_name: Lucia Marin Castro gender: F address: null formatted_address: null place_of_birth: null marital_status: UNKNOWN nationality: ESP extra_fields: first_surname: Marin second_surname: Castro mrz: {} parsed_address: null warnings: - risk: DOCUMENT_EXPIRED feature: ID_VERIFICATION additional_data: null log_type: error short_description: Document expired long_description: The document's expiration date has passed, rendering it no longer valid for use. barcodes: [] vendor_data: user-123 metadata: null created_at: '2026-06-12T02:25:48.117204+00:00' schema: type: object properties: request_id: type: string format: uuid description: Persisted session id when `save_api_request=true` (usable with `GET /v3/session/{sessionId}/decision/`); otherwise a transient correlation UUID. id_verification: type: object properties: status: type: string enum: - Approved - Declined description: '`Approved` when no warning resolves to a decline; `Declined` otherwise — the `warnings` list explains why.' document_type: type: string nullable: true description: 'Detected document type display name: `Identity Card`, `Passport`, `Driver''s License`, `Residence Permit`, `Health Insurance Card`, or `Tax Card`. `null` when the document could not be classified.' example: Identity Card document_subtype: type: string nullable: true description: Finer-grained subtype when available (e.g. `ID_CARD_GENERIC`, `EPASSPORT`). example: ID_CARD_GENERIC document_number: type: string nullable: true example: CBX164224 personal_number: type: string nullable: true description: Secondary personal/national number printed on some documents. example: 20446581H portrait_image: type: string nullable: true description: Cropped portrait photo from the document. A short-lived media URL (`https:///ocr/...`) when `save_api_request=true` (default); an inline base64-encoded JPEG string when `save_api_request=false`. `null` when no portrait was detected. front_image: type: string nullable: true description: Aligned crop of the document front. Media URL when saved, inline base64 JPEG otherwise. back_image: type: string nullable: true description: Aligned crop of the document back. `null` when no `back_image` was uploaded. front_image_camera_front: type: string nullable: true description: Always `null` on this endpoint (populated only in camera-capture verification sessions). back_image_camera_front: type: string nullable: true description: Always `null` on this endpoint. front_image_camera_front_face_match_score: type: number nullable: true description: Always `null` on this endpoint. back_image_camera_front_face_match_score: type: number nullable: true description: Always `null` on this endpoint. front_image_quality_score: type: object nullable: true description: Capture-quality diagnostics for the uploaded image (`null` when the side was not provided or scoring failed). All scores are 0–100. properties: focus_score: type: number format: float description: Sharpness of the document crop (higher is sharper). brightness_score: type: number format: float description: Exposure quality (higher is better). brightness_issue: type: string nullable: true description: '`ok` when exposure is fine, otherwise the detected issue (e.g. too dark / too bright).' is_document_fully_visible: type: boolean nullable: true description: Whether all four corners of the document are inside the frame. resolution_score: type: number format: float description: Effective resolution of the document area. overall_score: type: number format: float description: Aggregate quality score. back_image_quality_score: type: object nullable: true description: Capture-quality diagnostics for the uploaded image (`null` when the side was not provided or scoring failed). All scores are 0–100. properties: focus_score: type: number format: float description: Sharpness of the document crop (higher is sharper). brightness_score: type: number format: float description: Exposure quality (higher is better). brightness_issue: type: string nullable: true description: '`ok` when exposure is fine, otherwise the detected issue (e.g. too dark / too bright).' is_document_fully_visible: type: boolean nullable: true description: Whether all four corners of the document are inside the frame. resolution_score: type: number format: float description: Effective resolution of the document area. overall_score: type: number format: float description: Aggregate quality score. date_of_birth: type: string format: date nullable: true example: '1980-01-13' age: type: integer nullable: true description: Holder age in years, computed from `date_of_birth`. example: 46 expiration_date: type: string format: date nullable: true example: '2032-03-31' date_of_issue: type: string format: date nullable: true example: '2022-03-31' issuing_state: type: string nullable: true description: Issuing country (ISO 3166-1 alpha-3). example: ESP issuing_state_name: type: string nullable: true example: Spain first_name: type: string nullable: true example: Julio Francisco last_name: type: string nullable: true example: Fores Sena full_name: type: string nullable: true example: Julio Francisco Fores Sena gender: type: string nullable: true description: '`M`, `F`, or `null` when not printed/detected.' example: M address: type: string nullable: true description: Address exactly as printed on the document. formatted_address: type: string nullable: true description: Geocoded, normalized version of `address`. place_of_birth: type: string nullable: true example: Valencia, Valencia marital_status: type: string nullable: true description: '`UNKNOWN` unless the document prints marital status.' example: UNKNOWN nationality: type: string nullable: true description: Holder nationality (ISO 3166-1 alpha-3). example: ESP extra_fields: type: object nullable: true additionalProperties: true description: Document-specific extras that have no dedicated column (e.g. `first_surname`/`second_surname` on Spanish IDs, license classes on driving licenses). mrz: type: object description: Parsed machine-readable zone. An **empty object** (`{}`) when the document has no MRZ or it could not be read — also check the `warnings` list in that case. Dates inside the MRZ use the raw `YYMMDD` format. properties: surname: type: string example: FORES SENA name: type: string example: JULIO FRANCISCO country: type: string description: Issuing country (ISO 3166-1 alpha-3). example: ESP nationality: type: string description: Holder nationality (ISO 3166-1 alpha-3). example: ESP birth_date: type: string description: Raw MRZ date of birth (`YYMMDD`). example: '800113' expiry_date: type: string description: Raw MRZ expiry date (`YYMMDD`). example: '320331' sex: type: string example: M document_type: type: string description: MRZ document-type code (e.g. `ID`, `P`). example: ID document_number: type: string example: CBX164224 optional_data: type: string nullable: true example: 20446581H optional_data_2: type: string nullable: true example: '' birth_date_hash: type: string description: MRZ check digit for the birth date. example: '9' expiry_date_hash: type: string description: MRZ check digit for the expiry date. example: '8' document_number_hash: type: string description: MRZ check digit for the document number. example: '3' final_hash: type: string description: Composite MRZ check digit. example: '3' personal_number: type: string nullable: true example: 20446581H warnings: type: array items: type: string description: Non-fatal MRZ parsing warnings. errors: type: array items: type: string description: MRZ check-digit/parsing errors. Non-empty errors surface as an `MRZ_VALIDATION_FAILED` warning. mrz_type: type: string description: 'MRZ layout: `TD1` (ID cards, 3 lines), `TD2`, or `TD3` (passports, 2 lines).' example: TD1 mrz_string: type: string description: Raw MRZ text as read, lines separated by `\n`. mrz_key: type: string description: Composite key (document number + check digits) used for cross-checks. parsed_address: type: object nullable: true description: Structured, geocoded breakdown of `address`. `null` when the document carries no address or it could not be parsed. properties: street_1: type: string nullable: true example: Avenida el Cardonal 52 street_2: type: string nullable: true example: b city: type: string nullable: true example: La Laguna region: type: string nullable: true example: Canarias country: type: string nullable: true description: ISO 3166-1 alpha-2 country code. example: ES postal_code: type: string nullable: true example: '38108' address_type: type: string nullable: true example: Avenida formatted_address: type: string nullable: true description: Canonical geocoded address string. example: Av. el Cardonal, 52, b, 38108 La Laguna, Santa Cruz de Tenerife, Spain raw_results: type: object additionalProperties: true description: Raw geocoding payload (`address_components`, `geometry.location`, `place_id`, …) for advanced consumers. document_location: type: object nullable: true properties: latitude: type: number longitude: type: number description: Geocoded coordinates of the document address. label: type: string nullable: true description: Which document field the address was read from. example: Spain Identity Card Address is_verified: type: boolean description: Whether geocoding confirmed the address exists. category: type: string nullable: true description: Address category (e.g. `Residential`, `Commercial`). example: Residential warnings: type: array description: 'Empty on a clean approval. Every entry explains one detected risk; entries with `log_type: "error"` set `status` to `Declined`.' items: type: object properties: risk: type: string description: 'Machine-readable risk code. Always-decline risks: `DOCUMENT_EXPIRED`, `MINIMUM_AGE_NOT_MET`, `PORTRAIT_IMAGE_NOT_DETECTED`, `SCREEN_CAPTURE_DETECTED`, `PRINTED_COPY_DETECTED`, `PORTRAIT_MANIPULATION_DETECTED`, `INVALID_DATE`, `COULD_NOT_RECOGNIZE_DOCUMENT` (400-only, never a warning), `DOCUMENT_NUMBER_NOT_DETECTED`, `COULD_NOT_DETECT_DOCUMENT_TYPE`, `NAME_NOT_DETECTED`, `DATE_OF_BIRTH_NOT_DETECTED`. Governed by request options: `MRZ_NOT_DETECTED` / `MRZ_VALIDATION_FAILED` (`invalid_mrz_action`), `DATA_INCONSISTENT` / `MRZ_AND_DATA_EXTRACTED_FROM_OCR_NOT_SAME` / `DOCUMENT_NAME_DIFFERENT_FROM_OTHER_APPROVED_DOCUMENTS` (`inconsistent_data_action`), `EXPIRATION_DATE_NOT_DETECTED` (`expiration_date_not_detected_action`). Other risks (e.g. `POSSIBLE_DUPLICATED_USER`) are informational.' example: DOCUMENT_EXPIRED feature: type: string enum: - ID_VERIFICATION description: Feature that raised the warning. Always `ID_VERIFICATION` on this endpoint. additional_data: type: object nullable: true additionalProperties: true description: Extra context for some risks (e.g. `POSSIBLE_DUPLICATED_USER` includes `duplicated_session_id`); `null` for most. log_type: type: string enum: - error - warning - information description: Severity. `error` warnings drive `status` to `Declined`; `information` entries are advisory and never decline on their own. (`warning` is not produced by this endpoint — its options only accept `DECLINE` or `NO_ACTION`.) short_description: type: string description: Human-readable one-line summary of the risk. long_description: type: string description: Human-readable explanation of the risk. barcodes: type: array description: Barcodes decoded from the document (PDF417, QR, …). Empty when the document has none or none could be read. items: type: object properties: type: type: string description: Barcode type (e.g. `PDF417`, `QR_CODE`, `UNKNOWN`). position: type: object nullable: true additionalProperties: true description: Location of the barcode in the image, when available. data: type: string description: Decoded payload. data_raw: type: string description: Raw (undecoded) payload, when different. side: type: string nullable: true description: '`front` or `back`.' vendor_data: type: string nullable: true description: Echo of the `vendor_data` you sent, or `null`. metadata: type: object nullable: true additionalProperties: true description: Echo of the `metadata` object you sent, or `null`. created_at: type: string format: date-time description: ISO 8601 timestamp (UTC) of when the response was generated. '400': description: 'Validation or processing error. Field-level problems return DRF''s standard envelope (one array of messages per offending field). Encrypted-PDF problems return `{"detail": ...}`. A document image in which no document can be recognized at all returns `{"error": "COULD_NOT_RECOGNIZE_DOCUMENT"}`. PDF uploads can additionally fail with "The submitted PDF file is corrupted or invalid." or a no-pages error.' content: application/json: examples: Missing front image: summary: '`front_image` not included in the form data' value: front_image: - No file was submitted. Unsupported file extension: summary: File extension outside tiff/jpg/jpeg/png/webp/pdf value: front_image: - 'File extension “txt” is not allowed. Allowed extensions are: tiff, jpg, jpeg, png, webp, pdf.' File too large: summary: Upload exceeds the 10 MB limit value: front_image: - File size should not exceed 10 MB Invalid option value: summary: Options only accept DECLINE or NO_ACTION value: invalid_mrz_action: - '"REVIEW" is not a valid choice.' Encrypted PDF: summary: PDF uploaded without the required password value: detail: The PDF is encrypted. Please upload a decrypted PDF or a photo instead. Wrong PDF password: summary: '`front_image_password` / `back_image_password` does not decrypt the PDF' value: detail: The PDF password is incorrect. Please provide the correct password. Document not recognized: summary: No identity document could be detected in the image value: error: COULD_NOT_RECOGNIZE_DOCUMENT PDF conversion failure: summary: Uploaded PDF could not be converted to an image value: error: Failed to convert front image PDF. '403': description: 'Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — and also when the calling organization''s balance cannot cover the call. Authentication failures return `403` with `{"detail": ...}`; this API never returns `401`. Credit shortfalls return `403` with `{"error": ...}` before any image processing happens.' content: application/json: examples: Missing or invalid API key: summary: No `x-api-key` header, or the key is invalid/revoked value: detail: You do not have permission to perform this action. Not enough credits: summary: Organization balance cannot cover the call value: error: You don't have enough credits to perform this request. Please top up at https://business.didit.me '429': description: Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers. content: application/json: examples: Write rate limit exceeded: summary: More than 300 write requests in the current minute value: detail: Write request rate limit exceeded. You can make up to 300 requests per minute. schema: type: object properties: detail: type: string security: - ApiKeyAuth: [] x-codeSamples: - lang: curl label: cURL source: "curl -X POST 'https://verification.didit.me/v3/id-verification/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -F 'front_image=@./id_front.jpg' \\\n -F 'back_image=@./id_back.jpg' \\\n -F 'perform_document_liveness=true' \\\n -F 'save_api_request=true' \\\n -F 'vendor_data=user-123'" - lang: python label: Python source: "import requests\n\nurl = 'https://verification.didit.me/v3/id-verification/'\nheaders = {'x-api-key': 'YOUR_API_KEY'}\n\nwith open('id_front.jpg', 'rb') as front_f, open('id_back.jpg', 'rb') as back_f:\n files = {\n 'front_image': ('id_front.jpg', front_f, 'image/jpeg'),\n 'back_image': ('id_back.jpg', back_f, 'image/jpeg'),\n }\n data = {\n 'perform_document_liveness': 'true',\n 'save_api_request': 'true',\n 'vendor_data': 'user-123',\n }\n resp = requests.post(url, headers=headers, files=files, data=data, timeout=120)\n\nresp.raise_for_status()\nid_v = resp.json()['id_verification']\nprint('status:', id_v['status'])\nprint('name:', id_v.get('full_name'))\nprint('document:', id_v.get('document_type'), id_v.get('document_number'))\nfor w in id_v.get('warnings', []):\n print('warning:', w['risk'], '-', w['log_type'])" - lang: javascript label: JavaScript source: "import fs from 'node:fs';\n\nconst form = new FormData();\nform.append('front_image', new Blob([fs.readFileSync('./id_front.jpg')]), 'id_front.jpg');\nform.append('back_image', new Blob([fs.readFileSync('./id_back.jpg')]), 'id_back.jpg');\nform.append('perform_document_liveness', 'true');\nform.append('save_api_request', 'true');\nform.append('vendor_data', 'user-123');\n\nconst response = await fetch('https://verification.didit.me/v3/id-verification/', {\n method: 'POST',\n headers: { 'x-api-key': 'YOUR_API_KEY' },\n body: form,\n});\n\nif (!response.ok) throw new Error(`ID verification failed: ${response.status}`);\nconst body = await response.json();\nconsole.log('status:', body.id_verification.status);\nconsole.log('warnings:', body.id_verification.warnings.map((w) => w.risk));" /v3/face-match/: post: summary: Face Match description: 'Compare two face images (1:1) and get back a similarity `score` (0–100) plus an `Approved`/`Declined` `status`. **How it works.** Both images are decoded and analyzed by a biometric model (set `rotate_image=true` if captures may be sideways or upside down), then a similarity score between the two faces is computed. A score **strictly above** `face_match_score_decline_threshold` (default `30`) returns `status: "Approved"`; a score at or below it returns `Declined` with a `LOW_FACE_MATCH_SIMILARITY` warning. If no face pair can be scored at all, the call still returns `200` with `status: "Declined"` and a `NO_REFERENCE_IMAGE` warning — `score` is `null` on the default save path, or `0` with `save_api_request=false`. Submit one clear, front-facing face per image for best results. **Billing.** Each `200` response consumes one Face Match API credit (standalone APIs have no free tier). When the organization''s balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any image processing. **Session persistence (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session: it appears in the Business Console, the returned `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/`, both images are stored as the session''s face-match source/target images, and a `status.updated` webhook is emitted to your configured webhook endpoints. When `false`, nothing is stored and `request_id` is a one-off correlation UUID that cannot be looked up later. **Face search index.** This endpoint never enrolls faces into your face search index and never runs blocklist or duplicate screening — use `POST /v3/passive-liveness/` or `POST /v3/face-search/` for those. **Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload, no session is persisted, and no credits are consumed. **Authentication.** Send your application''s API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`) — this API never returns `401`. **Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.' operationId: post_v3face-match tags: - Standalone APIs parameters: [] requestBody: required: true content: multipart/form-data: schema: type: object required: - user_image - ref_image properties: user_image: type: string format: binary description: 'Live or candidate face image to verify (e.g. a selfie). Submit a single front-facing photo with the subject''s face clearly visible and well lit. Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `webp`. Maximum upload size: **5 MB** (larger files are rejected with `400`). Images are automatically compressed to ~0.5 MB before processing, so very high resolutions do not improve accuracy.' ref_image: type: string format: binary description: Reference face image to compare against (e.g. an ID document portrait or a previously enrolled photo). Same format and size limits as `user_image`. face_match_score_decline_threshold: type: number format: float default: 30 minimum: 0 maximum: 100 description: Similarity threshold (0–100) that drives the response `status`. A computed score **at or below** this value sets `status` to `Declined` and adds a `LOW_FACE_MATCH_SIMILARITY` warning; a score strictly above it returns `Approved`. Default `30` is permissive — raise it (e.g. `50`–`60`) for stricter verification. Values outside 0–100 return `400`. example: 50 rotate_image: type: boolean default: false description: When `true`, the service tries the input images in 90-degree increments (0, 90, 180, 270) and uses the orientation that yields the best face detection — useful for mobile captures with missing EXIF orientation. Adds latency, so only enable it if you cannot guarantee upright images. example: false save_api_request: type: boolean default: true description: When `true` (default), persists the call as an API-type session — visible in the Business Console, retrievable via `GET /v3/session/{sessionId}/decision/` using the returned `request_id`, and announced through a `status.updated` webhook. When `false`, nothing is stored and `request_id` is a transient UUID for response correlation only. example: true vendor_data: type: string description: Optional opaque string (your internal user id, email, UUID…) stored on the persisted session and echoed back in the response. Use it to correlate API calls with your own records and to filter sessions later. example: user-123 metadata: type: object additionalProperties: true description: Optional JSON object stored with the session (when `save_api_request=true`) and echoed back in the response. In multipart requests, send it as a JSON-encoded string field (e.g. `metadata={"flow":"login"}`) — it is parsed into an object. example: flow: login device_id: abc123 example: user_image: (binary JPEG/PNG of the live selfie) ref_image: (binary JPEG/PNG of the reference photo) face_match_score_decline_threshold: 50 rotate_image: false save_api_request: true vendor_data: user-123 metadata: flow: login responses: '200': description: Face match computed. `face_match.score` is a 0–100 float (two decimals); `status` is `Approved` when the score is strictly above `face_match_score_decline_threshold`, otherwise `Declined` with an explanatory warning. A low similarity or unscorable pair still returns `200` — inspect `face_match.status` and `face_match.warnings`, not just the HTTP code. When `save_api_request=true`, `request_id` is the persisted session id. content: application/json: examples: Approved: summary: Same person — score above the threshold value: request_id: f9f5a777-8002-44c6-baaf-128bcfd6e226 face_match: status: Approved score: 99.41 user_image: entities: - bbox: - 661 - 728 - 1688 - 2188 confidence: 0.732973 age: 26.91 gender: male race: null best_angle: 0 ref_image: entities: - bbox: - 653 - 745 - 1691 - 2185 confidence: 0.722082 age: 27 gender: male race: null best_angle: 0 warnings: [] vendor_data: user-123 metadata: flow: login created_at: '2026-06-12T01:04:42.763237+00:00' Declined - low similarity: summary: Score at or below the decline threshold value: request_id: 7cdda24e-311a-46d3-a3bb-e5d9b564ce73 face_match: status: Declined score: 24.87 user_image: entities: - bbox: - 661 - 728 - 1688 - 2188 confidence: 0.732973 age: 26.91 gender: male race: null best_angle: 0 ref_image: entities: - bbox: - 156 - 234 - 679 - 898 confidence: 0.717775 age: 41.2 gender: male race: null best_angle: 0 warnings: - risk: LOW_FACE_MATCH_SIMILARITY feature: FACEMATCH additional_data: null log_type: error short_description: Low face match similarity long_description: The facial features of the provided image don't closely match the reference image, suggesting a potential identity mismatch. vendor_data: user-123 metadata: null created_at: '2026-06-12T01:08:19.859116+00:00' schema: type: object properties: request_id: type: string format: uuid description: Persisted session id when `save_api_request=true` (usable with `GET /v3/session/{sessionId}/decision/`); otherwise a transient correlation UUID. face_match: type: object properties: status: type: string enum: - Approved - Declined description: '`Approved` when the similarity score is strictly above `face_match_score_decline_threshold` and a score could be computed; `Declined` otherwise (a warning explains why).' score: type: number format: float nullable: true minimum: 0 maximum: 100 description: 'Similarity score from 0 to 100 — the model''s confidence that both images depict the same person. Rounded to two decimals only when `save_api_request` is true; with `save_api_request=false` the raw float is returned. `null` when no face pair could be scored (with the default `save_api_request=true`); with `save_api_request=false` the same condition can surface as `score: 0`. Either way a `NO_REFERENCE_IMAGE` warning is present and `status` is `Declined`.' example: 99.41 user_image: type: object description: Face-detection results for the analyzed images. properties: entities: type: array items: type: object properties: bbox: type: array items: type: integer minItems: 4 maxItems: 4 description: Bounding box of the detected face as `[x_min, y_min, x_max, y_max]` pixel coordinates in the processed image. example: - 661 - 728 - 1688 - 2188 confidence: type: number format: float minimum: 0 maximum: 1 description: Face-detection confidence (0–1). example: 0.732973 age: type: number format: float description: Model-estimated age of the detected face, in years. Informational only — it does not affect the face-match decision. example: 26.91 gender: type: string description: Model-predicted gender of the detected face (`male` or `female`). Informational only. example: male race: type: string nullable: true description: Reserved field — always `null` in responses from this endpoint. example: null description: One entry per detected face. best_angle: type: integer description: 'Rotation (degrees: 0, 90, 180, or 270) that produced the best face detection. Only non-zero when `rotate_image=true` corrected the orientation.' example: 0 ref_image: type: object description: Face-detection results for the reference image, same shape as `user_image`. properties: entities: type: array items: type: object properties: bbox: type: array items: type: integer minItems: 4 maxItems: 4 description: Bounding box of the detected face as `[x_min, y_min, x_max, y_max]` pixel coordinates in the processed image. example: - 661 - 728 - 1688 - 2188 confidence: type: number format: float minimum: 0 maximum: 1 description: Face-detection confidence (0–1). example: 0.732973 age: type: number format: float description: Model-estimated age of the detected face, in years. Informational only — it does not affect the face-match decision. example: 26.91 gender: type: string description: Model-predicted gender of the detected face (`male` or `female`). Informational only. example: male race: type: string nullable: true description: Reserved field — always `null` in responses from this endpoint. example: null description: One entry per detected face. best_angle: type: integer example: 0 warnings: type: array description: Empty on a clean approval. `NO_REFERENCE_IMAGE` (`error`) — no face pair could be scored; `LOW_FACE_MATCH_SIMILARITY` (`error`) — score at or below the decline threshold. Any of these sets `status` to `Declined`. items: type: object properties: risk: type: string enum: - NO_REFERENCE_IMAGE - LOW_FACE_MATCH_SIMILARITY description: Machine-readable risk code. feature: type: string enum: - FACEMATCH description: Feature that raised the warning. Always `FACEMATCH` on this endpoint. additional_data: type: object nullable: true additionalProperties: true description: Always `null` for face-match warnings. log_type: type: string enum: - error - warning - information description: Severity. `error` warnings drive `status` to `Declined`; `warning` and `information` entries are advisory and never decline on their own. short_description: type: string description: Human-readable one-line summary of the risk. long_description: type: string description: Human-readable explanation of the risk. vendor_data: type: string nullable: true description: Echo of the `vendor_data` you sent, or `null`. metadata: type: object nullable: true additionalProperties: true description: Echo of the `metadata` object you sent, or `null`. created_at: type: string format: date-time description: ISO 8601 timestamp (UTC) of when the response was generated, e.g. `2026-06-12T01:04:42.763237+00:00`. '400': description: 'Validation error. Returned when a required image is missing, exceeds 5 MB, has an unsupported extension, or when an option is out of range. The body is DRF''s standard field-error envelope: one array of messages per offending field.' content: application/json: examples: Missing required image: summary: '`ref_image` (or `user_image`) not included in the form data' value: ref_image: - No file was submitted. Unsupported file extension: summary: File extension outside tiff/jpg/jpeg/png/webp value: user_image: - 'File extension “txt” is not allowed. Allowed extensions are: tiff, jpg, jpeg, png, webp.' File too large: summary: Upload exceeds the 5 MB limit value: user_image: - File size should not exceed 5 MB Threshold out of range: summary: '`face_match_score_decline_threshold` outside 0–100' value: face_match_score_decline_threshold: - Ensure this value is less than or equal to 100. '403': description: 'Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — and also when the calling organization''s balance cannot cover the call. Authentication failures return `403` with `{"detail": ...}`; this API never returns `401`. Credit shortfalls return `403` with `{"error": ...}` before any image processing happens.' content: application/json: examples: Missing or invalid API key: summary: No `x-api-key` header, or the key is invalid/revoked value: detail: You do not have permission to perform this action. Not enough credits: summary: Organization balance cannot cover the call value: error: You don't have enough credits to perform this request. Please top up at https://business.didit.me '429': description: Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers. content: application/json: examples: Write rate limit exceeded: summary: More than 300 write requests in the current minute value: detail: Write request rate limit exceeded. You can make up to 300 requests per minute. schema: type: object properties: detail: type: string security: - ApiKeyAuth: [] x-codeSamples: - lang: curl label: cURL source: "curl -X POST 'https://verification.didit.me/v3/face-match/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -F 'user_image=@./selfie.jpg' \\\n -F 'ref_image=@./id_portrait.jpg' \\\n -F 'face_match_score_decline_threshold=50' \\\n -F 'save_api_request=true' \\\n -F 'vendor_data=user-123'" - lang: python label: Python source: "import requests\n\nurl = 'https://verification.didit.me/v3/face-match/'\nheaders = {'x-api-key': 'YOUR_API_KEY'}\n\nwith open('selfie.jpg', 'rb') as user_f, open('id_portrait.jpg', 'rb') as ref_f:\n files = {\n 'user_image': ('selfie.jpg', user_f, 'image/jpeg'),\n 'ref_image': ('id_portrait.jpg', ref_f, 'image/jpeg'),\n }\n data = {\n 'face_match_score_decline_threshold': 50,\n 'save_api_request': 'true',\n 'vendor_data': 'user-123',\n }\n resp = requests.post(url, headers=headers, files=files, data=data, timeout=60)\n\nresp.raise_for_status()\nbody = resp.json()\nprint('status:', body['face_match']['status'])\nprint('score:', body['face_match']['score'])" - lang: javascript label: JavaScript source: "import fs from 'node:fs';\n\nconst form = new FormData();\nform.append('user_image', new Blob([fs.readFileSync('./selfie.jpg')]), 'selfie.jpg');\nform.append('ref_image', new Blob([fs.readFileSync('./id_portrait.jpg')]), 'id_portrait.jpg');\nform.append('face_match_score_decline_threshold', '50');\nform.append('save_api_request', 'true');\nform.append('vendor_data', 'user-123');\n\nconst response = await fetch('https://verification.didit.me/v3/face-match/', {\n method: 'POST',\n headers: { 'x-api-key': 'YOUR_API_KEY' },\n body: form,\n});\n\nif (!response.ok) throw new Error(`Face match failed: ${response.status}`);\nconst body = await response.json();\nconsole.log('status:', body.face_match.status, 'score:', body.face_match.score);" /v3/age-estimation/: post: summary: Age Estimation (face age + passive liveness) description: 'Estimate a person''s age from a single face photo and run a passive liveness check in the same call — returns the model-predicted `age_estimation` (in years), a liveness `score` (0–100), and an `Approved`/`Declined` `status`. The age is **model-predicted from the face, not verified against a document** — use it for age gates and low-friction checks, not as legal proof of age. **How it works.** The image is analyzed for faces (set `rotate_image=true` if captures may be sideways or upside down); each detected face gets an estimated age, and the largest face in the frame drives `age_estimation`. In parallel, a passive liveness model scores whether the photo shows a live person (no user challenge required) and detects presentation attacks (photos of photos, screens, masks). Submit one clear, front-facing, well-lit face per image for best results. **Decision logic.** Any warning declines: `status` is `Approved` only when the `warnings` array is empty. A liveness `score` at or below `face_liveness_score_decline_threshold` (default `30`) adds `LOW_LIVENESS_SCORE`; a detected attack adds `LIVENESS_FACE_ATTACK`; no face found by the liveness model adds `NO_FACE_DETECTED`; no estimable face age adds `AGE_NOT_DETECTED` (`age_estimation` is `null`); an estimated age below `age_estimation_decline_threshold` (default `18`) adds `AGE_BELOW_MINIMUM`. Borderline ages near your threshold deserve a fallback to document-based verification — the model''s error grows for faces close to the limit. **Billing.** Each `200` response consumes one Age Estimation API credit (standalone APIs have no free tier). When the organization''s balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any image processing. **Session persistence (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session: it appears in the Business Console, the returned `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/` (the result is exposed there in `liveness_checks[]`, with `features: ["AGE_ESTIMATION"]`), the face crop and reference image are stored, the face embedding is added to your application''s face index (used by face search and duplicate detection), and a `status.updated` webhook is emitted. When `false`, nothing is stored and `request_id` is a one-off correlation UUID that cannot be looked up later. **Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload (`age_estimation: 30`, no warnings), no session is persisted, and no credits are consumed. **Authentication.** Send your application''s API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`) — this API never returns `401`. **Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.' operationId: post_v3age-estimation tags: - Standalone APIs parameters: [] requestBody: required: true content: multipart/form-data: schema: type: object required: - user_image properties: user_image: type: string format: binary description: 'Face photo to analyze (e.g. a selfie). Submit a single front-facing photo with the subject''s face clearly visible and well lit. Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `webp` (no PDF). Maximum upload size: **5 MB** (larger files are rejected with `400`). Images are automatically compressed to ~0.3 MB before processing, so very high resolutions do not improve accuracy.' face_liveness_score_decline_threshold: type: number format: float default: 30 minimum: 0 maximum: 100 description: Liveness threshold (0–100). A liveness `score` **at or below** this value adds a `LOW_LIVENESS_SCORE` warning and declines. Raise it for stricter anti-spoofing; values outside 0–100 return `400`. example: 30 age_estimation_decline_threshold: type: integer default: 18 minimum: 0 maximum: 100 description: Minimum acceptable estimated age in years. An estimated age **below** this value adds an `AGE_BELOW_MINIMUM` warning and declines. Set `0` to disable the age check (the call then only runs liveness + age estimation without an age-based decision). example: 18 rotate_image: type: boolean default: false description: When `true`, the service tries the input image in 90-degree increments (0, 90, 180, 270) and uses the orientation that yields the best face detection — useful for mobile captures with missing EXIF orientation. Adds latency, so only enable it if you cannot guarantee upright images. example: false save_api_request: type: boolean default: true description: When `true` (default), persists the call as an API-type session — visible in the Business Console, retrievable via `GET /v3/session/{sessionId}/decision/` using the returned `request_id`, announced through a `status.updated` webhook, and with the detected face added to your application's face index. When `false`, nothing is stored and `request_id` is a transient UUID for response correlation only. example: true vendor_data: type: string description: Optional opaque string (your internal user id, email, UUID…) stored on the persisted session and echoed back in the response. Use it to correlate API calls with your own records and to filter sessions later. example: user-123 metadata: type: object additionalProperties: true description: Optional JSON object stored with the session (when `save_api_request=true`) and echoed back in the response. In multipart requests, send it as a JSON-encoded string field (e.g. `metadata={"flow":"age-gate"}`) — it is parsed into an object. example: flow: age-gate example: user_image: (binary JPEG/PNG of the selfie) face_liveness_score_decline_threshold: 30 age_estimation_decline_threshold: 18 rotate_image: false save_api_request: true vendor_data: user-123 metadata: flow: age-gate responses: '200': description: 'Age estimation and passive liveness completed. `age_estimation.age_estimation` is the model-predicted age in years of the largest detected face (`null` if none); `age_estimation.score` is the passive-liveness score (0–100); `status` is `Approved` only when `warnings` is empty. An underage or spoofed capture still returns `200` with `status: "Declined"` — inspect the body, not just the HTTP code. When `save_api_request=true`, `request_id` is the persisted session id.' content: application/json: examples: Approved: summary: Live adult face — both checks passed value: request_id: 0c40ba43-64ab-4e2e-b4b8-7d1f12f81bc1 age_estimation: status: Approved method: PASSIVE score: 97.5 user_image: entities: - age: 27.33 bbox: - 40 - 40 - 100 - 100 confidence: 0.7177750468254089 gender: male best_angle: 0 age_estimation: 27.33 warnings: [] vendor_data: user-123 metadata: flow: age-gate created_at: '2026-06-12T02:24:11.512941+00:00' Declined - age below threshold: summary: Estimated age below `age_estimation_decline_threshold` value: request_id: 9be41a6f-2f4e-4f57-92f4-b3a4f6f0a1c2 age_estimation: status: Declined method: PASSIVE score: 95 user_image: entities: - age: 16.5 bbox: - 40 - 40 - 100 - 100 confidence: 0.7177750468254089 gender: female best_angle: 0 age_estimation: 16.5 warnings: - risk: AGE_BELOW_MINIMUM feature: LIVENESS additional_data: null log_type: error short_description: Age below minimum long_description: The age of the face is below the minimum age threshold for the application. vendor_data: user-123 metadata: null created_at: '2026-06-12T02:26:40.118332+00:00' Declined - liveness too low: summary: Liveness score at or below the decline threshold (possible spoof) value: request_id: 5f6f2f1f-7c4f-43b9-8d62-0a8f4c2d9e77 age_estimation: status: Declined method: PASSIVE score: 5 user_image: entities: - age: 25 bbox: - 40 - 40 - 100 - 100 confidence: 0.7177750468254089 gender: male best_angle: 0 age_estimation: 25 warnings: - risk: LOW_LIVENESS_SCORE feature: LIVENESS additional_data: null log_type: error short_description: Low liveness score long_description: The liveness check resulted in a low score, indicating potential use of non-live facial representations or poor-quality biometric data. vendor_data: user-123 metadata: null created_at: '2026-06-12T02:27:02.901547+00:00' schema: type: object properties: request_id: type: string format: uuid description: Persisted session id when `save_api_request=true` (usable with `GET /v3/session/{sessionId}/decision/`); otherwise a transient correlation UUID. age_estimation: type: object properties: status: type: string enum: - Approved - Declined description: '`Approved` when no warnings were raised; `Declined` otherwise (every warning this endpoint produces declines).' method: type: string enum: - PASSIVE description: Liveness method. Always `PASSIVE` on this endpoint (single-image, no user challenge). score: type: number format: float nullable: true minimum: 0 maximum: 100 description: Passive-liveness score from 0 to 100 — the model's confidence that the photo shows a live person. Compared against `face_liveness_score_decline_threshold`; at or below it the call declines with `LOW_LIVENESS_SCORE`. `null` when the liveness model returns no confidence — the call still declines with `LOW_LIVENESS_SCORE` because null is treated as 0 for the threshold comparison. example: 97.5 user_image: type: object description: Face-detection results for the analyzed image. properties: entities: type: array description: One entry per detected face. Empty when no face was found (then `age_estimation` is `null` and an `AGE_NOT_DETECTED` warning is added). items: type: object properties: age: type: number format: float description: Model-estimated age of this face, in years. example: 27.33 bbox: type: array items: type: integer minItems: 4 maxItems: 4 description: Bounding box of the detected face as `[x_min, y_min, x_max, y_max]` pixel coordinates in the processed image. example: - 40 - 40 - 100 - 100 confidence: type: number format: float minimum: 0 maximum: 1 description: Face-detection confidence (0–1). example: 0.7177750468254089 gender: type: string description: Model-predicted gender of the detected face (`male` or `female`). Informational only. example: male best_angle: type: integer nullable: true description: 'Rotation (degrees: 0, 90, 180, or 270) that produced the best face detection. Only non-zero when `rotate_image=true` corrected the orientation.' example: 0 age_estimation: type: number format: float nullable: true description: Model-predicted age in years of the **largest** detected face. `null` when no face age could be estimated (an `AGE_NOT_DETECTED` warning is added and the call declines). example: 27.33 warnings: type: array description: Empty on a clean approval. Any entry sets `status` to `Declined`. items: type: object properties: risk: type: string enum: - NO_FACE_DETECTED - LOW_LIVENESS_SCORE - LIVENESS_FACE_ATTACK - AGE_NOT_DETECTED - AGE_BELOW_MINIMUM description: Machine-readable risk code. `NO_FACE_DETECTED` — the liveness model found no face; `LOW_LIVENESS_SCORE` — liveness `score` at or below `face_liveness_score_decline_threshold`; `LIVENESS_FACE_ATTACK` — a presentation attack (photo/screen/mask) was detected; `AGE_NOT_DETECTED` — no face age could be estimated; `AGE_BELOW_MINIMUM` — estimated age below `age_estimation_decline_threshold`. `NO_FACE_DETECTED` and `LOW_LIVENESS_SCORE` are mutually exclusive — when no face is found only `NO_FACE_DETECTED` is emitted. feature: type: string enum: - LIVENESS description: Feature that raised the warning. Always `LIVENESS` on this endpoint. additional_data: type: object nullable: true additionalProperties: true description: Always `null` for age-estimation warnings. log_type: type: string enum: - error description: Severity. Every warning this endpoint produces is an `error` and sets `status` to `Declined`. short_description: type: string description: Human-readable one-line summary of the risk. long_description: type: string description: Human-readable explanation of the risk. vendor_data: type: string nullable: true description: Echo of the `vendor_data` you sent, or `null`. metadata: type: object nullable: true additionalProperties: true description: Echo of the `metadata` object you sent, or `null`. created_at: type: string format: date-time description: ISO 8601 timestamp (UTC) of when the response was generated. '400': description: 'Validation error. Returned when `user_image` is missing, exceeds 5 MB, has an unsupported extension, or when a threshold is out of range. The body is DRF''s standard field-error envelope: one array of messages per offending field.' content: application/json: examples: Missing user image: summary: '`user_image` not included in the form data' value: user_image: - No file was submitted. Unsupported file extension: summary: File extension outside tiff/jpg/jpeg/png/webp value: user_image: - 'File extension “txt” is not allowed. Allowed extensions are: tiff, jpg, jpeg, png, webp.' File too large: summary: Upload exceeds the 5 MB limit value: user_image: - File size should not exceed 5 MB Threshold out of range: summary: '`face_liveness_score_decline_threshold` outside 0–100' value: face_liveness_score_decline_threshold: - Ensure this value is less than or equal to 100. '403': description: 'Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — and also when the calling organization''s balance cannot cover the call. Authentication failures return `403` with `{"detail": ...}`; this API never returns `401`. Credit shortfalls return `403` with `{"error": ...}` before any image processing happens.' content: application/json: examples: Missing or invalid API key: summary: No `x-api-key` header, or the key is invalid/revoked value: detail: You do not have permission to perform this action. Not enough credits: summary: Organization balance cannot cover the call value: error: You don't have enough credits to perform this request. Please top up at https://business.didit.me '429': description: Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers. content: application/json: examples: Write rate limit exceeded: summary: More than 300 write requests in the current minute value: detail: Write request rate limit exceeded. You can make up to 300 requests per minute. schema: type: object properties: detail: type: string security: - ApiKeyAuth: [] x-codeSamples: - lang: curl label: cURL source: "curl -X POST 'https://verification.didit.me/v3/age-estimation/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -F 'user_image=@./selfie.jpg' \\\n -F 'age_estimation_decline_threshold=18' \\\n -F 'face_liveness_score_decline_threshold=30' \\\n -F 'save_api_request=true' \\\n -F 'vendor_data=user-123'" - lang: python label: Python source: "import requests\n\nurl = 'https://verification.didit.me/v3/age-estimation/'\nheaders = {'x-api-key': 'YOUR_API_KEY'}\n\nwith open('selfie.jpg', 'rb') as f:\n files = {'user_image': ('selfie.jpg', f, 'image/jpeg')}\n data = {\n 'age_estimation_decline_threshold': 18,\n 'face_liveness_score_decline_threshold': 30,\n 'save_api_request': 'true',\n 'vendor_data': 'user-123',\n }\n resp = requests.post(url, headers=headers, files=files, data=data, timeout=60)\n\nresp.raise_for_status()\nresult = resp.json()['age_estimation']\nprint('status:', result['status'])\nprint('estimated age:', result['age_estimation'])\nprint('liveness score:', result['score'])\nfor w in result['warnings']:\n print('warning:', w['risk'])" - lang: javascript label: JavaScript source: "import fs from 'node:fs';\n\nconst form = new FormData();\nform.append('user_image', new Blob([fs.readFileSync('./selfie.jpg')]), 'selfie.jpg');\nform.append('age_estimation_decline_threshold', '18');\nform.append('face_liveness_score_decline_threshold', '30');\nform.append('save_api_request', 'true');\nform.append('vendor_data', 'user-123');\n\nconst response = await fetch('https://verification.didit.me/v3/age-estimation/', {\n method: 'POST',\n headers: { 'x-api-key': 'YOUR_API_KEY' },\n body: form,\n});\n\nif (!response.ok) throw new Error(`Age estimation failed: ${response.status}`);\nconst { age_estimation } = await response.json();\nconsole.log('status:', age_estimation.status, 'age:', age_estimation.age_estimation, 'liveness:', age_estimation.score);" /v3/poa/: post: summary: Proof of Address (document OCR + address checks) description: 'Extract and validate a Proof of Address document — utility bills, bank statements, and government-issued documents — in one call. Returns the issuer, issue/expiry dates, the holder name, the raw and parsed/geocoded address, an `Approved`/`Declined` `status`, and a `warnings` list explaining every issue found. **Latency.** Extraction is LLM-based: typical calls take **5–15 seconds**, multi-page PDFs up to **~30 seconds**. Configure a client timeout of **at least 45 seconds** and do not retry before the call completes. **How it works.** The document (image or PDF, up to 15 MB; encrypted PDFs require `document_password`) is classified into a document type and subtype (e.g. `UTILITY_BILL` / `WATER_BILL`), and the issuer, dates, holder name, and address are extracted with LLM-based extraction. The address is parsed and geocoded into `poa_parsed_address`. PDF/EXIF forensics screen for tampering — modification after digital signing, known PDF editors, overlay-text manipulation, suspicious re-exports — surfacing `SUSPECTED_DOCUMENT_MANIPULATION` or `DOCUMENT_METADATA_MISMATCH`. Document age is validated against `poa_document_age_months`, and the language against `poa_languages_allowed`. If you pass `expected_address`, `expected_country`, or `expected_first_name`/`expected_last_name`, the extracted values are cross-checked and mismatches are flagged. **Decision logic.** `status` is `Approved` unless at least one warning resolves to a decline. Hard failures always decline: `MISSING_ADDRESS_INFORMATION`, `POA_DOCUMENT_EXPIRED`, `INVALID_DOCUMENT_TYPE`, `UNABLE_TO_VALIDATE_DOCUMENT_AGE`, plus `UNABLE_TO_EXTRACT_ISSUE_DATE` and `POA_NAME_NOT_DETECTED` (not configurable on this endpoint). Five risk groups are configurable per request via the `poa_*_action` options (`DECLINE` or `NO_ACTION`, all defaulting to `DECLINE`). `POA_DOCUMENT_NOT_SUPPORTED_FOR_APPLICATION` and `UNPARSABLE_OR_INVALID_ADDRESS` are informational on this endpoint and never decline. A document that cannot be processed at all returns `400`; a readable but problematic document returns `200` with `status: "Declined"` — always inspect `poa.status` and `poa.warnings`, not just the HTTP code. **Billing.** Each `200` response consumes one Proof of Address API credit (standalone APIs have no free tier). When the organization''s balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any document processing. **Session persistence (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session: it appears in the Business Console, the uploaded document is stored, the returned `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/` (the decision additionally exposes `document_file` and the `document_metadata` forensics, which this response omits), and a `status.updated` webhook is emitted. When `false`, nothing is stored and `request_id` is a one-off correlation UUID that cannot be looked up later. **Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload (a fictional "Sandbox Power Co." electricity bill), no session is persisted, and no credits are consumed. **Authentication.** Send your application''s API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`) — this API never returns `401`. **Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`. Note: `FUTURE_ISSUE_DATE` and `POOR_DOCUMENT_QUALITY` exist in workflow sessions but are never produced by this standalone endpoint — a future-dated document is not auto-declined here.' operationId: post_v3poa tags: - Standalone APIs parameters: [] requestBody: required: true content: multipart/form-data: schema: type: object required: - document properties: document: type: string format: binary description: 'The Proof of Address document. Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `pdf`, `webp`. Maximum upload size: **15 MB** (larger files are rejected with `400`). Multi-page PDFs are supported (expect higher latency); encrypted PDFs require `document_password`. Images are automatically compressed before processing.' document_password: type: string description: Password to decrypt `document` when it is an encrypted PDF. Sending an encrypted PDF without (or with a wrong) password returns `400`. writeOnly: true expected_address: type: string description: Address to verify against the document. It is geocoded and compared with the extracted address; a mismatch (or an expected address that cannot be geocoded/verified) adds `ADDRESS_MISMATCH_WITH_PROVIDED`, governed by `poa_address_mismatch_action`. The geocoded form is echoed back in `expected_details_formatted_address` / `expected_details_parsed_address`. example: Av. Monseñor Tavella 1291, Salta, Argentina expected_country: type: string description: Country to verify against the document (ISO 3166-1 code, e.g. `ARG` or `AR`). A mismatch with the document's country adds `POA_COUNTRY_MISMATCH_WITH_PROVIDED`, governed by `poa_address_mismatch_action`. example: ARG expected_first_name: type: string description: First name to verify against `name_on_document` (fuzzy match, transliteration-aware). A score below the match threshold adds `NAME_MISMATCH_WITH_PROVIDED`, governed by `poa_address_mismatch_action`. example: Sophia expected_last_name: type: string description: Last name to verify against `name_on_document`, combined with `expected_first_name` for the match score. example: Martinez poa_languages_allowed: type: string description: Comma-separated list of accepted document languages as ISO 639-1 codes (e.g. `en,es,fr`). When omitted or blank, **all 51 supported languages** are accepted. A document in a language outside the list adds `UNSUPPORTED_DOCUMENT_LANGUAGE` (governed by `poa_unsupported_language_action`); an unknown code in the list returns `400` with the full set of supported codes. example: en,es poa_document_age_months: type: string description: 'Comma-separated `key:value` pairs setting the maximum document age in months per document type — keys: `utility_bill`, `bank_statement`, `government_issued_document`, `other_poa_document`; values: `1`–`120`, or `-1` for unlimited (no age check). Defaults when omitted: `utility_bill:3,bank_statement:3,government_issued_document:12,other_poa_document:12`. A document older than its limit declines with `POA_DOCUMENT_EXPIRED`. **Caution:** when you provide this field, list *every* type you accept — omitted types are treated as not accepted (informational `POA_DOCUMENT_NOT_SUPPORTED_FOR_APPLICATION`) and their age check is skipped.' example: utility_bill:3,bank_statement:6 poa_name_mismatch_action: type: string enum: - DECLINE - NO_ACTION default: DECLINE description: Accepted but **currently not applied** — name-mismatch warnings (`NAME_MISMATCH_WITH_PROVIDED`) follow `poa_address_mismatch_action` instead. Set `poa_address_mismatch_action` to control both name and address mismatch behavior. poa_document_issues_action: type: string enum: - DECLINE - NO_ACTION default: DECLINE description: What to do on file-integrity problems — in practice only `DOCUMENT_METADATA_MISMATCH` is produced by this endpoint. poa_document_authenticity_action: type: string enum: - DECLINE - NO_ACTION default: DECLINE description: What to do when document manipulation is suspected (`SUSPECTED_DOCUMENT_MANIPULATION`) — e.g. the PDF was modified after digital signing, processed by a known PDF editor, shows overlay-text manipulation, or has inconsistent EXIF dates. poa_unsupported_language_action: type: string enum: - DECLINE - NO_ACTION default: DECLINE description: What to do when the document language is outside `poa_languages_allowed` (`UNSUPPORTED_DOCUMENT_LANGUAGE`). poa_address_mismatch_action: type: string enum: - DECLINE - NO_ACTION default: DECLINE description: 'What to do when the extracted details disagree with the `expected_*` fields: `ADDRESS_MISMATCH_WITH_PROVIDED`, `NAME_MISMATCH_WITH_PROVIDED`, and `POA_COUNTRY_MISMATCH_WITH_PROVIDED` all follow this action.' poa_issuer_not_identified_action: type: string enum: - DECLINE - NO_ACTION default: DECLINE description: What to do when no issuing company/organization can be identified on the document (`ISSUER_NOT_IDENTIFIED`). save_api_request: type: boolean default: true description: When `true` (default), persists the call as an API-type session — visible in the Business Console, retrievable via `GET /v3/session/{sessionId}/decision/` using the returned `request_id`, announced through a `status.updated` webhook, and with the uploaded document stored. When `false`, nothing is stored and `request_id` is a transient UUID for response correlation only. example: true vendor_data: type: string description: Optional opaque string (your internal user id, email, UUID…) stored on the persisted session and echoed back in the response. Use it to correlate API calls with your own records and to filter sessions later. example: user-123 metadata: type: object additionalProperties: true description: Optional JSON object stored with the session (when `save_api_request=true`) and echoed back in the response. In multipart requests, send it as a JSON-encoded string field (e.g. `metadata={"flow":"onboarding"}`) — it is parsed into an object. example: flow: onboarding example: document: (binary JPEG/PNG/PDF of the utility bill or bank statement) expected_address: Av. Monseñor Tavella 1291, Salta, Argentina expected_country: ARG expected_first_name: Sophia expected_last_name: Martinez poa_languages_allowed: en,es save_api_request: true vendor_data: user-123 responses: '200': description: 'Document processed. `poa.status` is `Approved` or `Declined`; every detected issue is itemized in `poa.warnings`. An expired, mismatched, or suspicious document still returns `200` with `status: "Declined"` — inspect the body, not just the HTTP code. When `save_api_request=true`, `request_id` is the persisted session id.' content: application/json: examples: Approved: summary: Utility bill read cleanly, no expected-details checks value: request_id: 4b523a8d-ed32-45e4-8aa8-a89eda459d88 poa: issuing_state: ARG document_type: UTILITY_BILL document_subtype: WATER_BILL document_language: es issuer: Aguas del Norte issue_date: '2026-04-10' expiration_date: null poa_address: 'AVDA. MONSEÑOR TAVELLA N° 1291, B° VILLA LAVALLE CP: 4400, CAPITAL - SALTA' poa_formatted_address: Av. Monseñor Tavella 1291 b, A4400 Salta, Argentina poa_parsed_address: street_1: Avenida Monseñor Tavella 1291 street_2: b city: Salta region: Salta country: AR postal_code: A4400 document_location: latitude: -24.8208682 longitude: -65.4130954 expected_details_address: null expected_details_formatted_address: null expected_details_parsed_address: null name_on_document: OESTE EMBOTELLADORA S.A. 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: - LA EMBOTELLADORA DEL NORTE SA status: Approved warnings: [] vendor_data: user-123 metadata: null created_at: '2026-06-12T02:29:02.439655+00:00' Declined - document too old: summary: Issue date older than the allowed `poa_document_age_months` value: request_id: f61681b8-9422-4378-ac3e-04c6a247fb45 poa: issuing_state: ARG document_type: UTILITY_BILL document_subtype: WATER_BILL document_language: es issuer: Aguas del Norte issue_date: '2021-02-10' expiration_date: null poa_address: 'AVDA. MONSEÑOR TAVELLA N° 1291, B° VILLA LAVALLE CP: 4400, CAPITAL - SALTA' poa_formatted_address: Av. Monseñor Tavella 1291 b, A4400 Salta, Argentina poa_parsed_address: street_1: Avenida Monseñor Tavella 1291 street_2: b city: Salta region: Salta country: AR postal_code: A4400 document_location: latitude: -24.8208682 longitude: -65.4130954 expected_details_address: null expected_details_formatted_address: null expected_details_parsed_address: null name_on_document: OESTE EMBOTELLADORA S.A. 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: - LA EMBOTELLADORA DEL NORTE SA status: Declined warnings: - risk: POA_DOCUMENT_EXPIRED feature: PROOF_OF_ADDRESS additional_data: max_age_months: 3 document_subtype: WATER_BILL document_type: UTILITY_BILL issue_date: '2021-02-10' log_type: error short_description: Document expired long_description: The submitted document is older than 90 days from its issue date, which exceeds the acceptable time period for validity. vendor_data: user-123 metadata: null created_at: '2026-06-12T02:23:02.165493+00:00' Declined - name mismatch: summary: '`expected_first_name`/`expected_last_name` do not match `name_on_document`' value: request_id: 0a7f2f9c-58f1-4b8e-b1e3-2f6f9d4f7a21 poa: issuing_state: ARG document_type: UTILITY_BILL document_subtype: WATER_BILL document_language: es issuer: Aguas del Norte issue_date: '2021-02-10' expiration_date: null poa_address: 'AVDA. MONSEÑOR TAVELLA N° 1291, B° VILLA LAVALLE CP: 4400, CAPITAL - SALTA' poa_formatted_address: Av. Monseñor Tavella 1291 b, A4400 Salta, Argentina poa_parsed_address: street_1: Avenida Monseñor Tavella 1291 street_2: b city: Salta region: Salta country: AR postal_code: A4400 document_location: latitude: -24.8208682 longitude: -65.4130954 expected_details_address: null expected_details_formatted_address: null expected_details_parsed_address: null name_on_document: OESTE EMBOTELLADORA S.A. 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: - LA EMBOTELLADORA DEL NORTE SA status: Declined warnings: - risk: NAME_MISMATCH_WITH_PROVIDED feature: PROOF_OF_ADDRESS additional_data: null log_type: error short_description: Name mismatch with provided information long_description: The full name on the document does not match the name from the user's verified identity documents, or the full name sent by API. vendor_data: user-123 metadata: null created_at: '2026-06-12T02:27:21.330172+00:00' schema: type: object properties: request_id: type: string format: uuid description: Persisted session id when `save_api_request=true` (usable with `GET /v3/session/{sessionId}/decision/`); otherwise a transient correlation UUID. poa: type: object properties: status: type: string enum: - Approved - Declined description: '`Approved` when no warning resolves to a decline; `Declined` otherwise — the `warnings` list explains why.' issuing_state: type: string nullable: true description: Country the document was issued in, as extracted from the document — usually ISO 3166-1 alpha-3 (e.g. `ARG`). example: ARG document_type: type: string enum: - UTILITY_BILL - BANK_STATEMENT - GOVERNMENT_ISSUED_DOCUMENT - OTHER_POA_DOCUMENT - UNKNOWN description: Detected document category. `UNKNOWN` means the document could not be classified as an acceptable Proof of Address and auto-declines with `INVALID_DOCUMENT_TYPE`. Never null — classification failures fall back to `UNKNOWN`. document_subtype: type: string description: Finer-grained classification within `document_type` — e.g. `ELECTRICITY_BILL`, `WATER_BILL`, `GAS_BILL`, `INTERNET_BILL`, `PHONE_BILL` (utility bills); `ACCOUNT_STATEMENT`, `CREDIT_CARD_STATEMENT` (bank statements); `TAX_ASSESSMENT`, `RESIDENCY_CERTIFICATE` (government documents). `UNKNOWN` when no subtype could be determined. Never null — classification failures fall back to `UNKNOWN`. example: WATER_BILL document_language: type: string nullable: true description: Detected document language (ISO 639-1). example: es issuer: type: string nullable: true description: Company or organization that issued the document. `null` adds `ISSUER_NOT_IDENTIFIED`. example: Aguas del Norte issue_date: type: string format: date nullable: true description: Document issue date (`YYYY-MM-DD`). `null` when it could not be extracted — that declines with `UNABLE_TO_EXTRACT_ISSUE_DATE` on this endpoint. example: '2021-02-10' expiration_date: type: string format: date nullable: true description: Explicit expiry/validity date printed on the document, when present. Independent of the `poa_document_age_months` age check. poa_address: type: string nullable: true description: Address exactly as printed on the document. `null` auto-declines with `MISSING_ADDRESS_INFORMATION`. poa_formatted_address: type: string nullable: true description: Geocoded, normalized version of `poa_address`. example: Av. Monseñor Tavella 1291 b, A4400 Salta, Argentina poa_parsed_address: type: object nullable: true description: Structured, geocoded breakdown of the document address. `null` when the address could not be parsed. properties: street_1: type: string nullable: true example: Avenida Monseñor Tavella 1291 street_2: type: string nullable: true city: type: string nullable: true example: Salta region: type: string nullable: true example: Salta country: type: string nullable: true description: ISO 3166-1 alpha-2 country code. example: AR postal_code: type: string nullable: true example: A4400 document_location: type: object nullable: true properties: latitude: type: number longitude: type: number description: Geocoded coordinates of the document address. expected_details_address: type: string nullable: true description: Echo of the `expected_address` you sent, or `null`. expected_details_formatted_address: type: string nullable: true description: Geocoded, normalized version of `expected_address`; `null` when not provided or not geocodable. expected_details_parsed_address: type: object nullable: true additionalProperties: true description: Full geocoding result for `expected_address` (street/city/region/country/postal code plus raw geocoder output and an `is_verified` flag). `null` when `expected_address` was not provided. name_on_document: type: string nullable: true description: Primary holder name extracted from the document; compared against `expected_first_name`/`expected_last_name` when provided. `null` declines with `POA_NAME_NOT_DETECTED` on this endpoint. example: OESTE EMBOTELLADORA S.A. extra_fields: type: object description: Document-specific extras. Bank fields are populated for bank statements when printed on the document; `null` otherwise. properties: bank_account_number: type: string nullable: true bank_iban: type: string nullable: true bank_sort_code: type: string nullable: true bank_routing_number: type: string nullable: true bank_swift_bic: type: string nullable: true bank_branch_name: type: string nullable: true bank_branch_address: type: string nullable: true document_phone_number: type: string nullable: true description: Contact phone number printed on the document. additional_names: type: array items: type: string description: Other holder names found on the document (e.g. co-account holders, previous account names). warnings: type: array description: 'Empty on a clean approval. Every entry explains one detected risk; entries with `log_type: "error"` set `status` to `Declined`.' items: type: object properties: risk: type: string description: 'Machine-readable risk code. Always-decline: `MISSING_ADDRESS_INFORMATION`, `POA_DOCUMENT_EXPIRED`, `INVALID_DOCUMENT_TYPE`, `UNABLE_TO_VALIDATE_DOCUMENT_AGE`, plus `UNABLE_TO_EXTRACT_ISSUE_DATE` and `POA_NAME_NOT_DETECTED` (fixed to decline on this endpoint). Governed by request options: `NAME_MISMATCH_WITH_PROVIDED` / `ADDRESS_MISMATCH_WITH_PROVIDED` / `POA_COUNTRY_MISMATCH_WITH_PROVIDED` (`poa_address_mismatch_action`), `POOR_DOCUMENT_QUALITY` (never produced by this endpoint) / `DOCUMENT_METADATA_MISMATCH` (`poa_document_issues_action`), `SUSPECTED_DOCUMENT_MANIPULATION` (`poa_document_authenticity_action`), `UNSUPPORTED_DOCUMENT_LANGUAGE` (`poa_unsupported_language_action`), `ISSUER_NOT_IDENTIFIED` (`poa_issuer_not_identified_action`). Informational on this endpoint: `POA_DOCUMENT_NOT_SUPPORTED_FOR_APPLICATION`, `UNPARSABLE_OR_INVALID_ADDRESS`.' example: POA_DOCUMENT_EXPIRED feature: type: string enum: - PROOF_OF_ADDRESS description: Feature that raised the warning. Always `PROOF_OF_ADDRESS` on this endpoint. additional_data: type: object nullable: true additionalProperties: true description: Extra context for some risks — e.g. `POA_DOCUMENT_EXPIRED` includes `max_age_months`, `document_type`, `document_subtype`, and `issue_date`; `SUSPECTED_DOCUMENT_MANIPULATION` includes `reason` and `detection_method`. `null` for most risks. log_type: type: string enum: - error - information description: Severity. `error` warnings drive `status` to `Declined`; `information` entries are advisory and never decline on their own. (`warning` is not produced by this endpoint — its options only accept `DECLINE` or `NO_ACTION`.) short_description: type: string description: Human-readable one-line summary of the risk. long_description: type: string description: Human-readable explanation of the risk. vendor_data: type: string nullable: true description: Echo of the `vendor_data` you sent, or `null`. metadata: type: object nullable: true additionalProperties: true description: Echo of the `metadata` object you sent, or `null`. created_at: type: string format: date-time description: ISO 8601 timestamp (UTC) of when the response was generated. '400': description: 'Validation or processing error. Field-level problems return DRF''s standard envelope (one array of messages per offending field). Encrypted-PDF problems return `{"detail": ...}`. A document the extraction pipeline cannot process returns `{"error": ["Error extracting POA information"]}`.' content: application/json: examples: Missing document: summary: '`document` not included in the form data' value: document: - No file was submitted. Unsupported file extension: summary: File extension outside tiff/jpg/jpeg/png/pdf/webp value: document: - 'File extension “txt” is not allowed. Allowed extensions are: tiff, jpg, jpeg, png, pdf, webp.' File too large: summary: Upload exceeds the 15 MB limit value: document: - File size should not exceed 15 MB Encrypted PDF: summary: PDF uploaded without the required password value: detail: The PDF is encrypted. Please upload a decrypted PDF or a photo instead. Wrong PDF password: summary: '`document_password` does not decrypt the PDF' value: detail: The PDF password is incorrect. Please provide the correct password. Invalid language code: summary: '`poa_languages_allowed` contains an unsupported code' value: poa_languages_allowed: - 'Invalid language code: ''xx''. Must be one of the supported languages: [''ar'', ''bn'', ''hy'', ''bg'', ''bs'', ''ca'', ''cnr'', ''sq'', ''zh'', ''hr'', ''cs'', ''da'', ''nl'', ''en'', ''et'', ''fi'', ''fr'', ''ka'', ''kk'', ''de'', ''el'', ''he'', ''hi'', ''hu'', ''id'', ''it'', ''ja'', ''ko'', ''ky'', ''lv'', ''lt'', ''mk'', ''mn'', ''ms'', ''no'', ''fa'', ''pl'', ''pt'', ''ro'', ''ru'', ''sr'', ''sk'', ''sl'', ''so'', ''es'', ''sv'', ''th'', ''tr'', ''uk'', ''uz'', ''vi''].' Invalid document age value: summary: '`poa_document_age_months` value outside 1–120 / -1' value: poa_document_age_months: - 'Invalid integer value for ''utility_bill'': ''999''. Must be -1 (unlimited) or a positive integer between 1 and 120.' Extraction failure: summary: The document could not be processed by the extraction pipeline value: error: - Error extracting POA information '403': description: 'Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — and also when the calling organization''s balance cannot cover the call. Authentication failures return `403` with `{"detail": ...}`; this API never returns `401`. Credit shortfalls return `403` with `{"error": ...}` before any document processing happens.' content: application/json: examples: Missing or invalid API key: summary: No `x-api-key` header, or the key is invalid/revoked value: detail: You do not have permission to perform this action. Not enough credits: summary: Organization balance cannot cover the call value: error: You don't have enough credits to perform this request. Please top up at https://business.didit.me '429': description: Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers. content: application/json: examples: Write rate limit exceeded: summary: More than 300 write requests in the current minute value: detail: Write request rate limit exceeded. You can make up to 300 requests per minute. schema: type: object properties: detail: type: string security: - ApiKeyAuth: [] x-codeSamples: - lang: curl label: cURL source: "curl -X POST 'https://verification.didit.me/v3/poa/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -F 'document=@./utility-bill.pdf' \\\n -F 'expected_address=Av. Monseñor Tavella 1291, Salta, Argentina' \\\n -F 'expected_country=ARG' \\\n -F 'expected_first_name=Sophia' \\\n -F 'expected_last_name=Martinez' \\\n -F 'poa_languages_allowed=en,es' \\\n -F 'vendor_data=user-123' \\\n --max-time 45" - lang: python label: Python source: "import requests\n\nurl = 'https://verification.didit.me/v3/poa/'\nheaders = {'x-api-key': 'YOUR_API_KEY'}\n\nwith open('utility-bill.pdf', 'rb') as f:\n files = {'document': ('utility-bill.pdf', f, 'application/pdf')}\n data = {\n 'expected_address': 'Av. Monseñor Tavella 1291, Salta, Argentina',\n 'expected_country': 'ARG',\n 'expected_first_name': 'Sophia',\n 'expected_last_name': 'Martinez',\n 'poa_languages_allowed': 'en,es',\n 'vendor_data': 'user-123',\n }\n # LLM-based extraction takes 5-15s (multi-page PDFs up to ~30s) - allow headroom\n resp = requests.post(url, headers=headers, files=files, data=data, timeout=45)\n\nresp.raise_for_status()\npoa = resp.json()['poa']\nprint('status:', poa['status'])\nprint('issuer:', poa['issuer'], '| issued:', poa['issue_date'])\nprint('address:', poa['poa_formatted_address'])\nfor w in poa['warnings']:\n print('warning:', w['risk'])" - lang: javascript label: JavaScript source: "import fs from 'node:fs';\n\nconst form = new FormData();\nform.append('document', new Blob([fs.readFileSync('./utility-bill.pdf')]), 'utility-bill.pdf');\nform.append('expected_address', 'Av. Monseñor Tavella 1291, Salta, Argentina');\nform.append('expected_country', 'ARG');\nform.append('expected_first_name', 'Sophia');\nform.append('expected_last_name', 'Martinez');\nform.append('poa_languages_allowed', 'en,es');\nform.append('vendor_data', 'user-123');\n\n// LLM-based extraction takes 5-15s (multi-page PDFs up to ~30s) - allow >=45s\nconst response = await fetch('https://verification.didit.me/v3/poa/', {\n method: 'POST',\n headers: { 'x-api-key': 'YOUR_API_KEY' },\n body: form,\n signal: AbortSignal.timeout(45_000),\n});\n\nif (!response.ok) throw new Error(`POA verification failed: ${response.status}`);\nconst { poa } = await response.json();\nconsole.log('status:', poa.status);\nconsole.log('warnings:', poa.warnings.map((w) => w.risk));" /v3/aml/: post: summary: AML screening (standalone) operationId: post_v3aml tags: - Standalone APIs security: - ApiKeyAuth: [] parameters: [] description: 'Screen a person or company against PEP, sanctions, warning/watchlist, and adverse-media datasets in one call. **Two-score model.** Each hit carries a `match_score` (0–100, identity confidence) and a `risk_score` (0–100, entity risk). Hits whose `match_score` is below `aml_match_score_threshold` (default **93**) are tagged `review_status: "False Positive"` and ignored for risk purposes; the highest `risk_score` among the remaining hits becomes `aml.score`. The final `aml.status` follows your thresholds: `score` > `aml_score_review_threshold` (default **100**) → `Declined`; `score` > `aml_score_approve_threshold` (default **80**) → `In Review`; otherwise `Approved`. Whenever `score` is non-zero and ≥ the approve threshold a `POSSIBLE_MATCH_FOUND` warning is added (a score of exactly 0 never warns). **Match-score weights.** `aml_name_weight` + `aml_dob_weight` + `aml_country_weight` must sum to exactly 100 (defaults 60/25/15). Components missing from the request are re-normalized away; a matching `document_number` acts as a golden key that overrides `match_score` to 100. **Latency.** Plain screenings typically complete in a few seconds. With `include_adverse_media=true` the API waits for adverse-media enrichment (an initial ~5 s pause plus up to ~25 s of polling) — configure a client timeout of **at least 60 seconds**. **Persistence.** `save_api_request` defaults to **true**: the screening is stored as a session (`request_id` works with `GET /v3/session/{sessionId}/decision/`), it appears in the console, a `status.updated` webhook fires, and hit review statuses can be updated later. With `save_api_request=false` nothing is stored and `request_id` is a transient correlation id. `include_ongoing_monitoring=true` keeps re-screening the profile and requires `save_api_request=true` (enforced with a `400`). **Billing.** One AML API credit per call, charged after a successful screening; an insufficient balance returns `403` before any screening happens. **Sandbox.** Keys from sandbox applications skip screening and billing entirely and return a static `Approved` response with zero hits.' requestBody: required: true content: application/json: schema: type: object required: - full_name properties: full_name: type: string description: Full name of the person or company to screen. example: Antonio Ejemplo Modelo entity_type: type: string enum: - person - company default: person description: Type of entity to screen. Defaults to `person`. date_of_birth: type: string format: date description: Date of birth (persons) or incorporation date (companies), `YYYY-MM-DD`. Improves match precision via the DOB weight. example: '1972-02-29' nationality: type: string description: ISO 3166-1 **alpha-2** country code (e.g. `ES`). Invalid codes return `400`. Improves match precision via the country weight. example: ES document_number: type: string description: 'Identity document number. Not weighted — acts as a golden key: an exact match overrides the hit''s `match_score` to 100, a hard mismatch penalizes it.' aml_score_approve_threshold: type: number minimum: 0 maximum: 100 default: 80 description: Risk-score threshold at or below which the result is `Approved`. Must be ≤ `aml_score_review_threshold` (else `400`). aml_score_review_threshold: type: number minimum: 0 maximum: 100 default: 100 description: Risk-score threshold above which the result is `Declined`. Scores between the two thresholds produce `In Review`. aml_name_weight: type: integer minimum: 0 maximum: 100 default: 60 description: Weight of name similarity in the per-hit match score. The three weights must sum to exactly 100 (else `400`). aml_dob_weight: type: integer minimum: 0 maximum: 100 default: 25 description: Weight of date of birth in the per-hit match score. aml_country_weight: type: integer minimum: 0 maximum: 100 default: 15 description: Weight of country/nationality in the per-hit match score. aml_match_score_threshold: type: integer minimum: 0 maximum: 100 default: 93 description: 'Per-hit cutoff: hits with `match_score` below this are `False Positive` (excluded from risk assessment); at or above are `Unreviewed` possible matches.' include_adverse_media: type: boolean default: false description: Also search news media for negative coverage. Adds up to ~30 s of latency while results are gathered — use a ≥60 s client timeout. include_ongoing_monitoring: type: boolean default: false description: Keep re-screening this profile and push changes via webhook. Requires `save_api_request=true` (else `400`). save_api_request: type: boolean default: true description: Persist the screening as a session (console visibility, decision endpoint, webhooks, hit review). Set to `false` for a stateless check. vendor_data: type: string description: Your identifier for the screened user; echoed back and stored with the session. metadata: type: object nullable: true description: Free-form JSON stored with the request and echoed back. x-codeSamples: - lang: curl label: curl source: "curl -X POST https://verification.didit.me/v3/aml/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"full_name\": \"Antonio Ejemplo Modelo\",\n \"entity_type\": \"person\",\n \"date_of_birth\": \"1972-02-29\",\n \"nationality\": \"ES\",\n \"include_adverse_media\": false,\n \"vendor_data\": \"user-1234\"\n }'" - lang: python label: Python source: "import os, requests\n\nresp = requests.post(\n \"https://verification.didit.me/v3/aml/\",\n headers={\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Content-Type\": \"application/json\",\n },\n json={\n \"full_name\": \"Antonio Ejemplo Modelo\",\n \"entity_type\": \"person\",\n \"date_of_birth\": \"1972-02-29\",\n \"nationality\": \"ES\",\n \"include_adverse_media\": False,\n \"vendor_data\": \"user-1234\",\n },\n timeout=60, # adverse media enrichment can add ~30s\n)\nresp.raise_for_status()\nprint(resp.json()[\"aml\"][\"status\"]) # Approved / In Review / Declined" - lang: javascript label: JavaScript source: "const res = await fetch('https://verification.didit.me/v3/aml/', {\n method: 'POST',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n full_name: 'Antonio Ejemplo Modelo',\n entity_type: 'person',\n date_of_birth: '1972-02-29',\n nationality: 'ES',\n include_adverse_media: false,\n vendor_data: 'user-1234',\n }),\n});\nconst data = await res.json();\nconsole.log(data.aml.status);" responses: '200': description: Screening completed. `aml.status` reflects your thresholds; every hit at or above the internal inclusion floor is listed in `aml.hits` with its identity `match_score`, entity `risk_score`, and a `score_breakdown` explaining the match. content: application/json: schema: type: object properties: request_id: type: string format: uuid description: Persisted session id when `save_api_request=true` (usable with `GET /v3/session/{sessionId}/decision/`); otherwise a transient correlation UUID. aml: type: object properties: status: type: string enum: - Approved - In Review - Declined description: '`Declined` when `score` > `aml_score_review_threshold`; `In Review` when `score` > `aml_score_approve_threshold`; `Approved` otherwise (including when no score is available).' total_hits: type: integer description: Number of entries in `hits`. entity_type: type: string enum: - person - company description: Echo of the screened entity type. hits: type: array items: type: object description: 'One screening hit. `match_score` (0–100) measures identity confidence — hits below `aml_match_score_threshold` are tagged `review_status: "False Positive"` and excluded from the risk assessment; hits at or above it are `"Unreviewed"` possible matches. `risk_score` (0–100) measures how risky the matched entity is and drives the top-level `aml.score`.' properties: id: type: string nullable: true description: Stable identifier of the matched entity in the screening datasets. url: type: string nullable: true description: Source URL for the matched entity, when available. match: type: boolean nullable: true description: Legacy exact-match flag. Prefer `match_score` + `review_status`. score: type: number nullable: true description: DEPRECATED weighted match score in the 0–1 range. Use `match_score` instead. match_score: type: number nullable: true description: Identity confidence (0–100). Compared against `aml_match_score_threshold` to split False Positives from Possible Matches. risk_score: type: number nullable: true description: Entity risk score (0–100). The highest `risk_score` among non-false-positive hits becomes the top-level `aml.score`. target: type: boolean nullable: true caption: type: string nullable: true description: Display name of the matched entity. datasets: type: array nullable: true items: type: string description: Datasets the entity appears in (e.g. `PEP`, `PEP Level 1`, sanctions list names). features: type: object nullable: true properties: type: object nullable: true description: 'Entity profile: `name`, `alias`, `notes`, `gender`, `birthDate`, `birthPlace`, `nationality`, `position`, and other biographical arrays (each nullable).' rca_name: type: string nullable: true description: Name of the related close associate through which the entity was matched, when the hit is an RCA. pep_matches: type: array description: PEP list entries backing this hit. items: type: object properties: list_name: type: string publisher: type: string source_url: type: string description: type: string matched_name: type: string pep_position: type: string date_of_birth: type: string place_of_birth: type: string aliases: type: array items: type: string education: type: array items: type: string other_sources: type: array items: type: string sanction_matches: type: array items: type: object description: Sanctions list entries backing this hit (same backing-list shape as `pep_matches`, plus sanction-specific fields). warning_matches: type: array items: type: object description: Warning/watchlist entries backing this hit. adverse_media_matches: type: array description: Adverse-media articles. Populated only when `include_adverse_media=true`. items: type: object properties: headline: type: string summary: type: string sentiment: type: string sentiment_score: type: number source_url: type: string thumbnail: type: string nullable: true country: type: string nullable: true author_name: type: string nullable: true publication_date: type: string nullable: true adverse_keywords: type: array items: type: string other_sources: type: array items: type: string adverse_media_details: type: object nullable: true description: Aggregated adverse-media sentiment for the entity (`sentiment`, `sentiment_score`, `adverse_keywords` frequency map). Populated only when `include_adverse_media=true`. first_seen: type: string format: date-time nullable: true description: When the entity first appeared in the datasets. last_seen: type: string format: date-time nullable: true description: Most recent dataset refresh that included the entity. linked_entities: type: array description: Known relationships of the matched entity (family, associates, companies). items: type: object properties: name: type: array items: type: string relation: type: array items: type: string status: type: array items: type: string details: type: array items: type: string active: type: array items: type: string risk_view: type: object nullable: true description: Per-dimension risk breakdown (`crimes`, `countries`, `categories`, `custom_list`), each with `score`, `weightage`, `risk_level`, and a `risk_scores` map. additional_information: type: object nullable: true description: Free-form extra context, e.g. a `flag_summary` array explaining why the entity is flagged. review_status: type: string nullable: true enum: - False Positive - Unreviewed - Confirmed Match - Inconclusive description: '`False Positive` — `match_score` below `aml_match_score_threshold`, excluded from risk assessment. `Unreviewed` — possible match awaiting review. `Confirmed Match` / `Inconclusive` — set manually by a reviewer (via the Business Console) on saved sessions; that review capability is not part of this spec.' score_breakdown: type: object nullable: true description: 'How `match_score` was computed. Name, date of birth, and country each contribute `_score` × `_weight_normalized` points; weights are re-normalized when a component is missing from the screened data. The document number is not weighted — it acts as a ''golden key'': `MATCH` overrides the score to 100, `NEUTRAL` leaves it unchanged, `HARD_MISMATCH` applies a penalty.' properties: name_score: type: integer nullable: true description: Raw name similarity (0–100). name_weight: type: integer nullable: true description: Configured name weight (`aml_name_weight`). name_weight_normalized: type: number nullable: true description: Name weight after re-normalization (0–100). name_contribution: type: number nullable: true description: Points the name contributed to `total_score`. dob_score: type: integer nullable: true description: Date-of-birth score as a percentage of its weight (−100 to 100). dob_weight: type: integer nullable: true description: Configured DOB weight (`aml_dob_weight`). dob_weight_normalized: type: number nullable: true description: DOB weight after re-normalization (0–100). dob_contribution: type: number nullable: true description: Points the DOB contributed to `total_score`. country_score: type: integer nullable: true description: Country score as a percentage of its weight (−100 to 100). country_weight: type: integer nullable: true description: Configured country weight (`aml_country_weight`). country_weight_normalized: type: number nullable: true description: Country weight after re-normalization (0–100). country_contribution: type: number nullable: true description: Points the country contributed to `total_score`. document_number_match_type: type: string nullable: true enum: - MATCH - NEUTRAL - HARD_MISMATCH description: Golden-key result for the document number comparison. document_number_effect: type: string nullable: true description: Human-readable description of the document-number effect. total_score: type: integer nullable: true description: Final weighted match score (0–100) — mirrors `match_score`. score: type: number nullable: true description: 'Overall AML risk score (0–100): the highest `risk_score` among hits that are not False Positives. Drives `status` via the two thresholds.' screened_data: type: object description: 'Echo of what was screened: `full_name`, `date_of_birth`, `nationality`, `document_number` (absent fields are `null`).' properties: full_name: type: string date_of_birth: type: string nullable: true nationality: type: string nullable: true document_number: type: string nullable: true warnings: type: array items: type: object properties: feature: type: string description: Feature that raised the warning. risk: type: string description: Machine-readable risk identifier. additional_data: type: object nullable: true description: Extra context for the warning, when available. log_type: type: string description: '`warning`, `information`, or `error` — how the risk affected the outcome.' short_description: type: string description: One-line human-readable summary. long_description: type: string description: Full human-readable explanation. description: Contains a `POSSIBLE_MATCH_FOUND` warning whenever `score` is non-zero and ≥ `aml_score_approve_threshold` (a score of exactly 0 never warns); empty otherwise. vendor_data: type: string nullable: true description: Echo of the `vendor_data` you sent. metadata: type: object nullable: true description: Echo of the `metadata` you sent. created_at: type: string format: date-time examples: No hits (Approved): summary: Clean screening — zero hits value: request_id: d27c5509-7a66-45a9-b05e-d69d9f4c336a aml: status: Approved total_hits: 0 entity_type: person hits: [] score: 0 screened_data: full_name: Maximilian Mustermann-Doe date_of_birth: '1992-03-14' nationality: DE document_number: null warnings: [] vendor_data: user-1234 metadata: internal_ref: abc-123 created_at: '2026-06-11T10:21:07.013938+00:00' PEP hit (Approved — risk below approve threshold): summary: One PEP possible match; risk score 73 stays under the default approve threshold of 80 value: request_id: 0b7e7a3e-65a1-4f3d-9c7a-1de2f9a40111 aml: status: Approved total_hits: 1 entity_type: person hits: - id: kDpfszrbkDdaxpMVZw3NLg url: https://news.example.org/articles/sample match: false score: 0.93 match_score: 93 risk_score: 73 target: null caption: Antonio Ejemplo Modelo datasets: - PEP Level 1 - PEP features: null rca_name: '' first_seen: '2026-01-07T00:00:00' last_seen: '2026-01-07T00:00:00' review_status: Unreviewed properties: name: - Antonio Ejemplo Modelo - Antonio Ejemplo alias: - Antonio Ejemplo Modelo - Ejemplo Modelo, Pedro notes: - Prime Minister of Spain gender: - male birthDate: - '1972-02-29' birthPlace: - Madrid, Spain nationality: null position: null risk_view: crimes: score: 0 weightage: 20 risk_level: Low risk_scores: {} countries: score: 9.64 weightage: 30 risk_level: Low risk_scores: Spain: 25.71 categories: score: 62.5 weightage: 50 risk_level: High risk_scores: PEP: 100 PEP Level 1: 100 custom_list: {} score_breakdown: name_score: 89 name_weight: 60 name_weight_normalized: 60 name_contribution: 53.4 dob_score: 100 dob_weight: 25 dob_weight_normalized: 25 dob_contribution: 25 country_score: 100 country_weight: 15 country_weight_normalized: 15 country_contribution: 15 document_number_match_type: NEUTRAL document_number_effect: No document number provided for screening total_score: 93 pep_matches: - aliases: [] education: [] list_name: Presidency of the Government of Spain publisher: Presidency of the Government of Spain source_url: https://news.example.org/articles/sample description: The Presidency of the Government is the central institutional body in Spain that supports the Prime Minister and coordinates the general action of the central government. matched_name: Antonio Ejemplo Modelo pep_position: '' date_of_birth: '1972-02-29' other_sources: [] place_of_birth: Madrid, Spain sanction_matches: [] warning_matches: [] adverse_media_matches: [] adverse_media_details: null linked_entities: - name: - Maria Begona Gomez Fernandez active: [] status: [] details: [] relation: - spouse additional_information: flag_summary: - Antonio Ejemplo Modelo has been flagged as a Level 1 Politically Exposed Person (PEP) from Spain, serving as the President of the Government of Spain. score: 73 screened_data: full_name: Pedro Sanchez Perez-Castejon date_of_birth: '1972-02-29' nationality: ES document_number: null warnings: [] vendor_data: null metadata: null created_at: '2026-06-11T10:23:44.812000+00:00' '400': description: 'Validation error. Field-level problems return DRF''s standard envelope (one array of messages per offending field); cross-field rules (weight sum, threshold ordering, monitoring-without-save) return `{"error": [...]}`.' content: application/json: examples: Missing full_name: summary: '`full_name` is the only required field' value: full_name: - This field is required. Invalid nationality: summary: '`nationality` must be ISO 3166-1 alpha-2' value: nationality: - Invalid ISO 3166-1 alpha-2 country code. Weights do not sum to 100: summary: '`aml_name_weight` + `aml_dob_weight` + `aml_country_weight` ≠ 100' value: error: - 'AML weights must sum to 100. Current sum: 90 (name: 50, dob: 25, country: 15)' Thresholds out of order: summary: Approve threshold above review threshold value: error: - Approve threshold (90.0) cannot be greater than review threshold (80.0) Monitoring without saving: summary: '`include_ongoing_monitoring=true` with `save_api_request=false`' value: error: - Ongoing monitoring can only be enabled when the API request is saved. '403': description: 'Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — and also when the calling organization''s balance cannot cover the call. Authentication failures return `403` with `{"detail": ...}`; this API never returns `401`. Credit shortfalls return `403` with `{"error": ...}` before any screening or provider call happens.' content: application/json: examples: Missing or invalid API key: summary: No `x-api-key` header, or the key is invalid/revoked value: detail: You do not have permission to perform this action. Not enough credits: summary: Organization balance cannot cover the call value: error: You don't have enough credits to perform this request. Please top up at https://business.didit.me '429': description: Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers. content: application/json: examples: Write rate limit exceeded: summary: More than 300 write requests in the current minute value: detail: Write request rate limit exceeded. You can make up to 300 requests per minute. schema: type: object properties: detail: type: string /v3/face-search/: post: summary: Face Search description: 'Search a face image (1:N) against your application''s face search index — faces enrolled from verification sessions, saved standalone API calls, direct user-profile face uploads, and your block/allow lists — and get back ranked similarity matches plus blocklist and duplicate warnings. **How it works.** The largest detected face in `user_image` is embedded and searched against the index, scoped to your application. Up to **5 matches** above the similarity floor are returned in `matches`, each with `similarity_percentage`, the originating session (`session_id`, `session_number`, `status`, `vendor_data`), `user_details` extracted during that session''s document verification, `is_blocklisted` / `is_allowlisted` flags, and a `source` discriminator (`session`, `imported`, or `list_entry`). - `search_type=most_similar` (default) ranks every enrolled face by similarity — use it for deduplication and fraud-ring investigation. - `search_type=blocklisted_or_approved` restricts candidates to blocklisted faces, allowlisted faces, faces from approved sessions, and imported user-profile faces, ranking blocklisted entries first — use it when the primary goal is blocklist screening. `status` is `Declined` only when a `FACE_IN_BLOCKLIST` or `POSSIBLE_FACE_IN_BLOCKLIST` warning fires; `DUPLICATED_FACE` / `POSSIBLE_DUPLICATED_FACE` (`information`) and `MULTIPLE_FACES_DETECTED` (`warning`) never decline. Unlike Passive Liveness, this endpoint does **not** exclude prior sessions with the same `vendor_data` from matching. A passive-liveness analysis also runs internally and is stored with the persisted session, but its result is not part of this response. **Billing.** Each `200` response consumes one Face Search API credit (standalone APIs have no free tier). Insufficient balance returns `403` before any image processing. **Session persistence and face enrollment (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session (Business Console, `GET /v3/session/{sessionId}/decision/` via the returned `request_id`, `status.updated` webhook) and the searched face is enrolled into your face search index. Faces enrolled by Face Search calls are excluded from future Face Search results, so repeated searches never match each other. When `false`, the call is one-shot: nothing is stored, the face is not enrolled, and `match_image_url` values are internal storage paths rather than downloadable URLs. **Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload, no session is persisted, and no credits are consumed. **Authentication.** Send your application''s API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`) — this API never returns `401`. **Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.' operationId: post_v3face-search tags: - Standalone APIs parameters: [] requestBody: required: true content: multipart/form-data: schema: type: object required: - user_image properties: user_image: type: string format: binary description: 'Front-facing face image to search with. Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `webp`. Maximum upload size: **5 MB** (larger files are rejected with `400`). Images are automatically compressed to ~0.5 MB before processing, so very high resolutions do not improve accuracy. The image must contain at least one detectable face — otherwise the endpoint returns `400`. When several faces are present, the largest one is searched and a `MULTIPLE_FACES_DETECTED` warning is added.' search_type: type: string default: most_similar enum: - most_similar - blocklisted_or_approved description: Search policy. `most_similar` (default) ranks every enrolled face in your application by similarity. `blocklisted_or_approved` restricts candidates to blocklisted faces, allowlisted faces, faces from approved sessions, and imported user-profile faces — with blocklisted entries ranked first. example: most_similar rotate_image: type: boolean default: false description: When `true`, the service tries 90-degree rotations of the input and uses the orientation that yields the best face detection. Useful when EXIF orientation is missing. Adds latency. example: false save_api_request: type: boolean default: true description: When `true` (default), persists the call as an API-type session, emits a `status.updated` webhook, presigns `match_image_url` values, and enrolls the searched face into your face search index (Face Search enrollments are excluded from future searches). When `false`, the search is one-shot — no session is stored and the face is not enrolled. example: true vendor_data: type: string description: Optional opaque identifier (your user id, email, UUID…) stored on the persisted session and echoed back. It does not exclude same-user faces from the search results. example: user-123 metadata: type: object additionalProperties: true description: Optional JSON object stored with the session (when `save_api_request=true`) and echoed back. In multipart requests, send it as a JSON-encoded string field — it is parsed into an object. example: flow: dedup_check example: user_image: (binary JPEG/PNG selfie) search_type: most_similar rotate_image: false save_api_request: true vendor_data: user-123 metadata: flow: dedup_check responses: '200': description: Face search completed. `face_search.matches` holds up to 5 similar faces ordered by similarity; an empty array means no enrolled face exceeded the similarity floor. `status` is `Declined` only on blocklist hits — duplicate matches alone return `Approved`, so inspect `matches` and `warnings`, not just `status`. When `save_api_request=true`, `request_id` is the persisted session id. content: application/json: examples: Blocklist hit (Declined): summary: The face matches a blocklisted face value: request_id: a1b2c3d4-e5f6-7890-1234-567890abcdef face_search: status: Declined total_matches: 1 matches: - session_id: 882c42d5-8a4d-4d20-8080-a22f57822c86 session_number: 323442 similarity_percentage: 99.99 source: session vendor_data: user-1 verification_date: '2025-01-01T00:00:00Z' user_details: full_name: Jane Marie Doe document_type: ID document_number: X1234567 match_image_url: https:///face/3f6a1c2e/reference.jpg status: Approved is_blocklisted: true is_allowlisted: false api_service: null user_image: entities: - bbox: - 40 - 40 - 120 - 120 confidence: 0.732973 best_angle: 0 warnings: - risk: FACE_IN_BLOCKLIST feature: LIVENESS additional_data: blocklisted_session_id: 882c42d5-8a4d-4d20-8080-a22f57822c86 blocklisted_session_number: 323442 api_service: null log_type: error short_description: Face in blocklist long_description: The system identified a face in the blocklist, which means the face is not allowed to be verified. vendor_data: user-123 metadata: null created_at: '2026-06-12T01:04:42.763237+00:00' Duplicate found (Approved): summary: The face already appeared in another approved session — informational, no decline value: request_id: 5e0c3a1f-7b2d-4c8e-9f10-2a3b4c5d6e7f face_search: status: Approved total_matches: 1 matches: - session_id: 1f2e3d4c-5b6a-7980-1122-334455667788 session_number: 1024 similarity_percentage: 87.42 source: session vendor_data: user-9 verification_date: '2025-11-20T09:15:00Z' user_details: null match_image_url: https:///face/9c8b7a6d/reference.jpg status: Approved is_blocklisted: false is_allowlisted: false api_service: PASSIVE_LIVENESS user_image: entities: - bbox: - 40 - 40 - 120 - 120 confidence: 0.732973 best_angle: 0 warnings: - risk: DUPLICATED_FACE feature: LIVENESS additional_data: duplicated_session_id: 1f2e3d4c-5b6a-7980-1122-334455667788 duplicated_session_number: 1024 api_service: PASSIVE_LIVENESS log_type: information short_description: Duplicated face from other approved session long_description: The system identified a duplicated face from another approved session, requiring further investigation. vendor_data: user-123 metadata: null created_at: '2026-06-12T01:04:42.763237+00:00' No matches (Approved): summary: No enrolled face exceeded the similarity floor value: request_id: 9d8c7b6a-5f4e-3d2c-1b0a-998877665544 face_search: status: Approved total_matches: 0 matches: [] user_image: entities: - bbox: - 40 - 40 - 120 - 120 confidence: 0.732973 best_angle: 0 warnings: [] vendor_data: user-123 metadata: null created_at: '2026-06-12T01:04:42.763237+00:00' schema: type: object properties: request_id: type: string format: uuid description: Persisted session id when `save_api_request=true` (usable with `GET /v3/session/{sessionId}/decision/`); otherwise a transient correlation UUID. face_search: type: object properties: status: type: string enum: - Approved - Declined description: '`Declined` only when a `FACE_IN_BLOCKLIST` or `POSSIBLE_FACE_IN_BLOCKLIST` warning fired. Duplicate matches and multiple-face warnings never decline.' total_matches: type: integer description: Number of entries in `matches` (0–5). example: 1 matches: type: array maxItems: 5 description: Up to 5 faces from your face search index that exceed the similarity floor, ordered by similarity (with blocklisted entries ranked first when `search_type=blocklisted_or_approved`). Empty when no enrolled face is similar enough. items: type: object properties: session_id: type: string format: uuid nullable: true description: Id of the verification session the matched face belongs to. `null` when `source` is `imported` or `list_entry`. example: 882c42d5-8a4d-4d20-8080-a22f57822c86 session_number: type: integer nullable: true description: Human-friendly session number shown in the Business Console. `null` for non-session matches. example: 323442 similarity_percentage: type: number format: float minimum: 0 maximum: 100 description: Similarity between the submitted face and the matched face (0–100). example: 99.99 source: type: string enum: - session - imported - list_entry description: 'Where the matched face was enrolled from: `session` — a verification session or a saved standalone API call; `imported` — a face uploaded directly to a user profile via the vendor-user faces upload endpoint; `list_entry` — a face attached to one of your lists.' vendor_data: type: string nullable: true description: '`vendor_data` of the matched session (or of the user profile for imported faces).' example: user-1 verification_date: type: string format: date-time nullable: true description: When the matched face was originally captured (`YYYY-MM-DDThh:mm:ssZ`). `null` for `list_entry` matches. example: '2025-01-01T00:00:00Z' user_details: type: object nullable: true description: 'Identity data extracted during the matched session''s document verification. `null` when the matched session has no document data (e.g. liveness-only sessions). For `source: imported` matches it is populated from the vendor-user profile instead: `full_name` set, `document_type`/`document_number` null.' properties: full_name: type: string nullable: true example: Jane Marie Doe document_type: type: string nullable: true example: ID document_number: type: string nullable: true example: X1234567 match_image_url: type: string nullable: true description: Time-limited URL (`https:///...`) of the matched face's reference image when `save_api_request=true`. With `save_api_request=false` this is an internal storage path that is not directly downloadable. example: https:///face/3f6a1c2e/reference.jpg status: type: string nullable: true description: Status of the matched session (`Approved`, `Declined`, `In Review`, …). `null` for `imported`/`list_entry` matches. example: Approved is_blocklisted: type: boolean description: '`true` when the matched face is on your face blocklist. Any sufficiently similar blocklisted match declines the search.' is_allowlisted: type: boolean description: '`true` when the matched face is on one of your allowlists. Allowlisted matches suppress duplicate warnings.' api_service: type: string nullable: true description: Set when the matched face was enrolled by a standalone API call (e.g. `PASSIVE_LIVENESS`); `null` for regular verification sessions and imported faces. example: null user_image: type: object description: Face-detection results for the submitted image. Entries here carry only `bbox` and `confidence`. properties: entities: type: array description: One entry per detected face. The largest face is the one searched. items: type: object properties: bbox: type: array items: type: integer minItems: 4 maxItems: 4 description: Bounding box `[x_min, y_min, x_max, y_max]` in pixels. example: - 40 - 40 - 120 - 120 confidence: type: number format: float minimum: 0 maximum: 1 description: Face-detection confidence (0–1). example: 0.732973 best_angle: type: integer description: Rotation (degrees) that produced the best face detection; non-zero only when `rotate_image=true` corrected the orientation. Never null on this endpoint (defaults to 0 when the model omits it). example: 0 warnings: type: array description: Risk signals. `FACE_IN_BLOCKLIST` / `POSSIBLE_FACE_IN_BLOCKLIST` (`error`) decline the search; `MULTIPLE_FACES_DETECTED` (`warning`) and `DUPLICATED_FACE` / `POSSIBLE_DUPLICATED_FACE` (`information`) are advisory. items: type: object properties: risk: type: string enum: - MULTIPLE_FACES_DETECTED - FACE_IN_BLOCKLIST - POSSIBLE_FACE_IN_BLOCKLIST - DUPLICATED_FACE - POSSIBLE_DUPLICATED_FACE description: Machine-readable risk code. feature: type: string enum: - LIVENESS description: Feature that raised the warning. Always `LIVENESS` on this endpoint. additional_data: type: object nullable: true additionalProperties: true description: '`null` for `MULTIPLE_FACES_DETECTED`. Blocklist hits carry `{blocklisted_session_id, blocklisted_session_number, api_service}`; duplicate hits carry `{duplicated_session_id, duplicated_session_number, api_service}` pointing at the first matching session.' log_type: type: string enum: - error - warning - information description: Severity. `error` warnings drive `status` to `Declined`; `warning` and `information` entries are advisory and never decline on their own. short_description: type: string description: Human-readable one-line summary of the risk. long_description: type: string description: Human-readable explanation of the risk. vendor_data: type: string nullable: true description: Echo of the `vendor_data` you sent, or `null`. metadata: type: object nullable: true additionalProperties: true description: Echo of the `metadata` object you sent, or `null`. created_at: type: string format: date-time description: ISO 8601 timestamp (UTC) of when the response was generated, e.g. `2026-06-12T01:04:42.763237+00:00`. '400': description: 'Validation error. Returned when `user_image` is missing, exceeds 5 MB, has an unsupported extension, or when **no face is detected** in the submitted image. Field errors use DRF''s `{field: [messages]}` envelope; face-detection failures use `{"error": ...}`.' content: application/json: examples: No face detected: summary: The image decodes fine but contains no detectable face value: error: No face detected in the image Missing image: summary: '`user_image` not included in the form data' value: user_image: - No file was submitted. Unsupported file extension: summary: File extension outside tiff/jpg/jpeg/png/webp value: user_image: - 'File extension “txt” is not allowed. Allowed extensions are: tiff, jpg, jpeg, png, webp.' File too large: summary: Upload exceeds the 5 MB limit value: user_image: - File size should not exceed 5 MB '403': description: 'Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — and also when the calling organization''s balance cannot cover the call. Authentication failures return `403` with `{"detail": ...}`; this API never returns `401`. Credit shortfalls return `403` with `{"error": ...}` before any image processing happens.' content: application/json: examples: Missing or invalid API key: summary: No `x-api-key` header, or the key is invalid/revoked value: detail: You do not have permission to perform this action. Not enough credits: summary: Organization balance cannot cover the call value: error: You don't have enough credits to perform this request. Please top up at https://business.didit.me '429': description: Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers. content: application/json: examples: Write rate limit exceeded: summary: More than 300 write requests in the current minute value: detail: Write request rate limit exceeded. You can make up to 300 requests per minute. schema: type: object properties: detail: type: string security: - ApiKeyAuth: [] x-codeSamples: - lang: curl label: cURL source: "curl -X POST 'https://verification.didit.me/v3/face-search/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -F 'user_image=@./selfie.jpg' \\\n -F 'search_type=most_similar' \\\n -F 'save_api_request=true' \\\n -F 'vendor_data=user-123'" - lang: python label: Python source: "import requests\n\nurl = 'https://verification.didit.me/v3/face-search/'\nheaders = {'x-api-key': 'YOUR_API_KEY'}\n\nwith open('selfie.jpg', 'rb') as f:\n files = {'user_image': ('selfie.jpg', f, 'image/jpeg')}\n data = {\n 'search_type': 'most_similar',\n 'save_api_request': 'true',\n 'vendor_data': 'user-123',\n }\n resp = requests.post(url, headers=headers, files=files, data=data, timeout=60)\n\nresp.raise_for_status()\nbody = resp.json()\nprint('status:', body['face_search']['status'])\nprint('matches:', body['face_search']['total_matches'])\nfor m in body['face_search']['matches']:\n print(f\" session={m['session_id']} similarity={m['similarity_percentage']} blocklisted={m['is_blocklisted']}\")" - lang: javascript label: JavaScript source: "import fs from 'node:fs';\n\nconst form = new FormData();\nform.append('user_image', new Blob([fs.readFileSync('./selfie.jpg')]), 'selfie.jpg');\nform.append('search_type', 'most_similar');\nform.append('save_api_request', 'true');\nform.append('vendor_data', 'user-123');\n\nconst response = await fetch('https://verification.didit.me/v3/face-search/', {\n method: 'POST',\n headers: { 'x-api-key': 'YOUR_API_KEY' },\n body: form,\n});\n\nif (!response.ok) throw new Error(`Face search failed: ${response.status}`);\nconst body = await response.json();\nconsole.log('status:', body.face_search.status, 'matches:', body.face_search.total_matches);\nbody.face_search.matches.forEach(m =>\n console.log(` session=${m.session_id} similarity=${m.similarity_percentage} blocklisted=${m.is_blocklisted}`)\n);" /v3/kyb/search/: post: summary: KYB registry company search (standalone) operationId: post_v3_kyb_search tags: - Standalone APIs security: - ApiKeyAuth: [] parameters: [] description: 'Search official company registries for candidate companies by name and/or registration number. This is step 1 of the two-step registry flow: **search** returns lightweight candidates, then `POST /v3/kyb/select/` retrieves the full profile of one candidate. **The search is free** — no balance check, no billing. Candidates always come back with `fetch_status: "pending"`: the full company data is only fetched (and billed) on select. **`kyb_response_id` is ephemeral.** It is a per-search handle, not a stable company id. Select promptly after searching and re-search to get a fresh handle instead of storing it. **Two modes.** - *Synchronous (default):* the API polls the registry until the search resolves and returns the final candidate list (`search_resolved: true`). - *Asynchronous:* pass `webhook_url` and the API returns immediately — usually with `search_status: "pending"` and an empty candidate list — then POSTs a `kyb.registry_search.resolved` callback to your URL when the registry finishes. The callback body is `{"event_id", "event_type": "kyb.registry_search.resolved", "request_id", "vendor_data", "metadata", "search_status", "search_resolved", "kyb_registry", "timestamp", "created_at"}` — note `timestamp` and `created_at` in the callback are epoch integers, unlike this endpoint''s ISO `created_at` — and is sent **unsigned** (header `X-Didit-Unsigned-Callback: true`, no `X-Signature`). **Country codes.** ISO 3166-1 alpha-2 (`GB`, `DE`), plus `XX-YY` subdivision codes where registries are regional — e.g. `US-CA` for California. **Sandbox.** Keys from sandbox applications return one static candidate without contacting any registry.' requestBody: required: true content: application/json: schema: type: object required: - country_code properties: country_code: type: string maxLength: 10 description: 'ISO 3166-1 alpha-2 code of the country whose company registry to search. For example, `GB`. Where company registries operate at state or province level, append the ISO 3166-2 subdivision code as `XX-YY`: `US-CA` searches the California registry. United States searches always require the subdivision — a bare `US` is rejected. Case-insensitive; normalized to upper case.' example: GB name: type: string maxLength: 255 description: Company name to search for. Either `name` or `registration_number` is required. example: Tesco registration_number: type: string maxLength: 100 description: Registry registration number to search for. Either `name` or `registration_number` is required. search_type: type: string enum: - contains - start_with - fuzzy default: contains description: Name-matching strategy applied by the registry search. vendor_data: type: string description: Your identifier for this lookup; echoed back and included in the webhook callback. metadata: type: object nullable: true description: Free-form JSON; echoed back and included in the webhook callback. webhook_url: type: string format: uri maxLength: 500 description: 'Switches the search to asynchronous mode: the API returns immediately and POSTs an unsigned `kyb.registry_search.resolved` callback to this URL when the candidate list is ready.' x-codeSamples: - lang: curl label: curl source: "curl -X POST https://verification.didit.me/v3/kyb/search/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"country_code\": \"GB\",\n \"name\": \"Tesco\",\n \"search_type\": \"contains\",\n \"vendor_data\": \"company-1234\"\n }'" - lang: python label: Python source: "import os, requests\n\nresp = requests.post(\n \"https://verification.didit.me/v3/kyb/search/\",\n headers={\"x-api-key\": os.environ[\"DIDIT_API_KEY\"]},\n json={\"country_code\": \"GB\", \"name\": \"Tesco\"},\n timeout=60, # synchronous mode waits for the registry to resolve\n)\nresp.raise_for_status()\ncandidates = resp.json()[\"kyb_registry\"][\"companies\"]\nkyb_response_id = candidates[0][\"kyb_response_id\"] # pass to /v3/kyb/select/" - lang: javascript label: JavaScript source: "const res = await fetch('https://verification.didit.me/v3/kyb/search/', {\n method: 'POST',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ country_code: 'GB', name: 'Tesco' }),\n});\nconst data = await res.json();\nconst kybResponseId = data.kyb_registry.companies[0].kyb_response_id;" responses: '200': description: 'Search accepted. Synchronous mode returns the resolved candidate list; webhook mode returns immediately (often `search_status: "pending"`) and delivers the final list via the `kyb.registry_search.resolved` callback.' content: application/json: schema: type: object properties: request_id: type: string format: uuid description: 'With `webhook_url`: the persisted search-request id, echoed as `request_id` in the `kyb.registry_search.resolved` callback. Without `webhook_url`: a transient correlation UUID (the search is not stored).' kyb_registry: type: object properties: companies: type: array items: type: object properties: kyb_response_id: type: string nullable: true description: 'Handle for this candidate — pass it to `POST /v3/kyb/select/` to retrieve the full profile. Ephemeral and scoped to this search: do not store it long-term; re-search to get a fresh one.' example: 69aeeb95febb0f1704042259 name: type: string nullable: true description: Registered company name. example: Tesco PLC registration_number: type: string nullable: true description: Registry registration number. example: 00445790 status: type: string nullable: true description: Normalized registry status of the company (e.g. `active`, `dissolved`, `inactive`, `struck off`). example: active type: type: string nullable: true description: Legal form as reported by the registry. example: Public Limited Company risk_level: type: string nullable: true description: Registry-reported risk level, when available. Usually `null` at search time. fetch_status: type: string nullable: true description: Always `pending` for search candidates — the full company profile is only fetched (and billed) when you select the candidate. example: pending description: Deduplicated candidate list. Empty while `search_resolved=false` (webhook mode) or when nothing matched. pagination: type: object properties: total: type: integer description: Number of candidates returned. page: type: integer per_page: type: integer example: 25 search_status: type: string nullable: true enum: - pending - resolved description: Registry search lifecycle status. search_resolved: type: boolean description: '`true` once the candidate list is final.' vendor_data: type: string nullable: true description: Echo of the `vendor_data` you sent. metadata: type: object nullable: true description: Echo of the `metadata` you sent. created_at: type: string format: date-time examples: Resolved search: summary: Synchronous mode — final candidate list value: request_id: 0c7e9a40-3a13-4f7e-8a44-9d2e2f1c5b6a kyb_registry: companies: - kyb_response_id: 69aeeb95febb0f1704042259 name: Tesco PLC registration_number: 00445790 status: active type: Public Limited Company risk_level: null fetch_status: pending - kyb_response_id: 69aeeb95febb0f170404225a name: E-TESCO LTD registration_number: '15168476' status: active type: Private Limited Company risk_level: null fetch_status: pending pagination: total: 2 page: 1 per_page: 25 search_status: resolved search_resolved: true vendor_data: company-1234 metadata: null created_at: '2026-06-11T10:40:00.000000+00:00' Pending search (webhook mode): summary: '`webhook_url` provided — candidates arrive via the kyb.registry_search.resolved callback' value: request_id: 5f2d8c1b-7e4a-4b9c-9f0d-3a6e8c2b1d4f kyb_registry: companies: [] pagination: total: 0 page: 1 per_page: 25 search_status: pending search_resolved: false vendor_data: company-1234 metadata: source: onboarding created_at: '2026-06-11T10:41:00.000000+00:00' '400': description: Validation error. Field-level problems use DRF's standard envelope; the name/registration-number rule reports under `non_field_errors`. content: application/json: examples: Missing search criteria: summary: Neither `name` nor `registration_number` provided value: non_field_errors: - Either 'name' or 'registration_number' is required. Invalid country_code: summary: Must be `XX` (alpha-2) or `XX-YY` subdivision value: country_code: - country_code must be a 2-letter ISO code. Invalid search_type: summary: Only contains / start_with / fuzzy value: search_type: - '"exact" is not a valid choice.' Invalid webhook_url: summary: '`webhook_url` must be a valid URL' value: webhook_url: - Enter a valid URL. '403': description: 'Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment. Authentication failures return `403` with `{"detail": ...}`; this API never returns `401`. This endpoint is free, so there is no credit-shortfall variant.' content: application/json: examples: Missing or invalid API key: summary: No `x-api-key` header, or the key is invalid/revoked value: detail: You do not have permission to perform this action. '429': description: Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers. content: application/json: examples: Write rate limit exceeded: summary: More than 300 write requests in the current minute value: detail: Write request rate limit exceeded. You can make up to 300 requests per minute. schema: type: object properties: detail: type: string /v3/kyb/select/: post: summary: KYB registry company select (standalone) operationId: post_v3_kyb_select tags: - Standalone APIs security: - ApiKeyAuth: [] parameters: [] description: 'Retrieve the full registry profile of one candidate returned by `POST /v3/kyb/search/` — company details, officers, beneficial owners, addresses, industries, filings, and the complete raw registry payload. **Pairing with search.** Pass the candidate''s `kyb_response_id` exactly as returned by the search. The handle is ephemeral and scoped to that search — select promptly, and re-search for a fresh handle rather than storing it. **Billing.** Each select is billed at the KYB registry price (search is free). The balance is checked before any registry call (`403` when short) and the charge is recorded against the created business session. **Persistence is mandatory.** Every select creates a business session (its id is the `request_id`) so billing and retrieval can be tracked; it appears in the console. `save_api_request` is accepted for symmetry but must be `true` (the default) — sending `false` returns `400`. **Resolution.** The API polls the registry for the full profile before responding. If the registry is still processing after polling, the response comes back with `fetch_status: "pending"` / `data_resolved: false` and sparse fields; the stored record completes automatically once the registry resolves (the business session status updates with it) — re-check it in the Business Console or by polling `GET /v3/session/{request_id}/decision/` (business sessions resolve through the same decision endpoint). **Status.** `kyb_registry.status` is Didit''s assessment of the registry check: companies that are not active in the registry, or whose officers/ownership could not be derived, are routed to `In Review`; clean active companies are `Approved`. `registry_status` is the company''s own status at the registry (e.g. `active`, `dissolved`). **Sandbox.** Keys from sandbox applications return a static resolved company without contacting any registry and without billing.' requestBody: required: true content: application/json: schema: type: object required: - kyb_response_id properties: kyb_response_id: type: string maxLength: 255 description: Candidate handle from `POST /v3/kyb/search/` (`kyb_registry.companies[].kyb_response_id`). Ephemeral — use it shortly after the search that produced it. example: 69aeeb95febb0f1704042259 vendor_data: type: string description: Your identifier for this company; echoed back and stored with the business session. metadata: type: object nullable: true description: Free-form JSON stored with the business session and echoed back. save_api_request: type: boolean default: true description: Must be `true` (the default). KYB registry selects are always persisted so billing and retrieval can be tracked; `false` returns `400`. x-codeSamples: - lang: curl label: curl source: "curl -X POST https://verification.didit.me/v3/kyb/select/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"kyb_response_id\": \"69aeeb95febb0f1704042259\",\n \"vendor_data\": \"company-1234\"\n }'" - lang: python label: Python source: "import os, requests\n\nsearch = requests.post(\n \"https://verification.didit.me/v3/kyb/search/\",\n headers={\"x-api-key\": os.environ[\"DIDIT_API_KEY\"]},\n json={\"country_code\": \"GB\", \"name\": \"Tesco\"},\n timeout=60,\n).json()\nkyb_response_id = search[\"kyb_registry\"][\"companies\"][0][\"kyb_response_id\"]\n\nresp = requests.post(\n \"https://verification.didit.me/v3/kyb/select/\",\n headers={\"x-api-key\": os.environ[\"DIDIT_API_KEY\"]},\n json={\"kyb_response_id\": kyb_response_id, \"vendor_data\": \"company-1234\"},\n timeout=90, # waits for the registry to resolve the full profile\n)\nresp.raise_for_status()\ncompany = resp.json()[\"kyb_registry\"]\nprint(company[\"company_name\"], company[\"registry_status\"], company[\"status\"])" - lang: javascript label: JavaScript source: "const res = await fetch('https://verification.didit.me/v3/kyb/select/', {\n method: 'POST',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n kyb_response_id: '69aeeb95febb0f1704042259',\n vendor_data: 'company-1234',\n }),\n});\nconst data = await res.json();\nconsole.log(data.kyb_registry.company_name, data.kyb_registry.registry_status);" responses: '200': description: Company retrieved and billed. `kyb_registry` carries the full profile; check `data_resolved` — when `false` the registry was still processing and the stored record completes automatically once it resolves. content: application/json: schema: type: object properties: request_id: type: string format: uuid description: Id of the business session created for this retrieval. The stored record (visible in the console) keeps updating if the registry resolves after the response. kyb_registry: type: object description: Full registry company profile. properties: uuid: type: string format: uuid description: Didit's id for this company record. node_id: type: string example: feature_kyb_registry status: type: string enum: - Not Finished - Approved - Declined - In Review description: Didit's assessment of the registry check (e.g. `In Review` when the company is not active in the registry or ownership data could not be derived). registry_status: type: string nullable: true enum: - active - dissolved - deregistered - see full details - authorised - appointed representative - unauthorised - inactive - no longer authorised - closed - struck off description: Normalized company status as reported by the registry. data_resolved: type: boolean description: '`true` once the registry returned the full profile. `false` when the registry was still processing after polling — the stored record completes automatically once the registry resolves.' company_name: type: string example: Tesco PLC registration_number: type: string nullable: true example: 00445790 country_code: type: string description: ISO 3166-1 alpha-2. example: GB region: type: string nullable: true description: Subdivision code when the registry is regional (e.g. `CA` for `US-CA`); empty otherwise. company_type: type: string nullable: true example: Public Limited Company incorporation_date: type: string format: date nullable: true example: '1947-11-27' registered_address: type: string nullable: true example: Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, United Kingdom, AL7 1GA tax_number: type: string nullable: true example: GB412 8925 93 risk_level: type: string nullable: true description: Registry-reported risk level, when available. verification_status: type: string nullable: true description: Registry verification status, when available. is_from_registry: type: boolean description: Always `true` for this endpoint — the profile was retrieved from an official registry. fetch_status: type: string enum: - pending - resolved description: Registry fetch lifecycle. Mirrored by `data_resolved`. alternative_names: type: string nullable: true description: Previous/alternative registered names, when reported. nature_of_business: type: string nullable: true description: Activity description or classification code, when reported. registered_capital: type: string nullable: true registered_capital_amount: type: string nullable: true description: Decimal amount as a string, when reported. registered_capital_currency: type: string nullable: true description: ISO 4217 code, when reported. website: type: string nullable: true example: https://www.tesco.com/ email: type: string nullable: true example: customer.service@tesco.co.uk phone: type: string nullable: true example: 03301231688 legal_entity_identifier: type: string nullable: true description: LEI, when reported. location_of_registration: type: string nullable: true description: Registry/jurisdiction the company is registered with, when reported. vat_number: type: string nullable: true description: EU VAT number, when collected during the hosted registry step or edited afterwards. At select time this is typically not yet set — VIES validation runs when the end user submits the registry step in the hosted flow. vat_validation_status: type: string enum: - valid - invalid - could_not_validate - not_applicable description: Result of the EU VIES check run at registry submit. `not_applicable` when no VAT number was provided or the company is outside the EU VAT area (EU-27 plus Northern Ireland). vat_validated_name: type: string nullable: true description: Trader name registered with VIES, when the member state discloses it. Only set when the VAT number is `valid`. vat_validated_address: type: string nullable: true description: Trader address registered with VIES, when the member state discloses it. Only set when the VAT number is `valid`. vat_checked_at: type: string format: date-time nullable: true description: Timestamp of the VIES check. `null` when no check ran. financial_summary: type: object nullable: true description: Registry-reported share/capital details (currency, total shares, share classes), when available. officers: type: array items: type: object properties: uuid: type: string format: uuid name: type: string example: MELISSA BETHELL designation: type: string nullable: true description: Raw registry designation (e.g. `Director`, `Secretary`). role: type: string description: Normalized role derived from the designation. Full 17-value model set; registry parsing on this endpoint typically emits director, non_executive_director, secretary, chairman, or company_officer. enum: - director - non_executive_director - secretary - chairman - ubo - shareholder - representative - founder - legal_advisor - authorized_signatory - trustee - beneficiary - company_officer - settlor - protector - investor - other nationality: type: string nullable: true is_active: type: boolean description: '`false` when the registry marks the appointment as resigned.' kyc_status: type: string nullable: true enum: - Approved - Declined - Pending description: Status of an associated KYC session, when one was started for this officer. `null` otherwise. kyc_session_url: type: string nullable: true description: Verification URL of the associated KYC session, when one exists. description: Directors, secretaries, and other officers extracted from the registry. beneficial_owners: type: array items: type: object properties: uuid: type: string format: uuid name: type: string first_name: type: string nullable: true last_name: type: string nullable: true entity_type: type: string enum: - person - company description: Whether the owner is a natural person or a corporate entity. roles: type: array items: type: string description: Normalized roles (e.g. `ubo`, `shareholder`). ownership_min_shares: type: string nullable: true description: Lower bound of the ownership band reported by the registry (e.g. `25%`). ownership_max_shares: type: string nullable: true description: Upper bound of the ownership band reported by the registry. is_active: type: boolean kyc_status: type: string nullable: true enum: - Approved - Declined - Pending kyc_session_url: type: string nullable: true effective_ownership_percent: type: number nullable: true description: Parsed ownership percentage (max bound, falling back to min bound). description: Beneficial owners / persons with significant control extracted from the registry. Can be empty when the registry does not expose ownership. addresses: type: array nullable: true items: type: object properties: address: type: string type: type: string description: type: string description: All addresses reported by the registry. industries: type: array nullable: true items: type: object description: Industry classifications, when reported. accounts: type: object nullable: true description: Filing/accounts dates reported by the registry (e.g. `last_account`, `next_account`, `due_by_date`). registry_data: type: object nullable: true description: The complete raw registry payload (people, filings, announcements, identifiers, …) for audit and custom parsing. user_provided_data: type: object nullable: true description: Always `null` on this endpoint (used by the hosted KYB flow when end users edit registry data). confirmed_by_user_at: type: string format: date-time nullable: true description: Always `null` on this endpoint. last_console_edit_at: type: string format: date-time nullable: true description: Set when a console reviewer edits the record later. is_editable: type: boolean description: Whether the registry-sourced data can still be edited in the hosted flow/console. vendor_data: type: string nullable: true description: Echo of the `vendor_data` you sent. metadata: type: object nullable: true description: Echo of the `metadata` you sent. created_at: type: string format: date-time examples: Resolved company: summary: Full registry profile (officer list truncated for brevity) value: request_id: 9d4f2c7a-1b3e-4a5c-8d6f-0e9a8b7c6d5e kyb_registry: uuid: f1e2d3c4-b5a6-4789-9c0d-1e2f3a4b5c6d node_id: feature_kyb_registry status: In Review registry_status: active data_resolved: true company_name: Tesco PLC registration_number: 00445790 country_code: GB region: '' company_type: Public Limited Company incorporation_date: '1947-11-27' registered_address: Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, United Kingdom, AL7 1GA tax_number: GB412 8925 93 risk_level: low risk verification_status: verified is_from_registry: true fetch_status: resolved alternative_names: TESCO STORES (HOLDINGS) PUBLIC LIMITED COMPANY nature_of_business: null registered_capital: null registered_capital_amount: null registered_capital_currency: null website: https://www.tesco.com/ email: customer.service@tesco.co.uk phone: 03301231688 legal_entity_identifier: null location_of_registration: null financial_summary: currency: Pound (£) total_number_of_shares: '8174378721' meta_detail: Class_Of_Share: Ordinary officers: - uuid: a1b2c3d4-e5f6-4789-9a0b-1c2d3e4f5a6b name: CHRISTOPHER JON TAYLOR designation: Secretary role: secretary nationality: null is_active: true kyc_status: null kyc_session_url: null - uuid: b2c3d4e5-f6a7-4890-8b1c-2d3e4f5a6b7c name: MELISSA BETHELL designation: Director role: director nationality: British is_active: true kyc_status: null kyc_session_url: null beneficial_owners: [] addresses: - address: Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, United Kingdom, AL7 1GA type: legal_entity_registry_address description: registered office address industries: null accounts: last_account: 26 February 2025 next_account: 26 February 2026 due_by_date: 26 August 2026 registry_data: name: Tesco PLC registration_number: 00445790 status: active type: Public Limited Company incorporation_date: '1947-11-27' tax_number: GB412 8925 93 country_code: GB fetch_status: resolved industries_detail: - code: '47110' name: Retail Sale In Non-specialised Stores With Food, Beverages Or Tobacco Predominating description: null meta_detail: [] user_provided_data: null confirmed_by_user_at: null last_console_edit_at: null is_editable: true vendor_data: company-1234 metadata: null created_at: '2026-06-11T10:45:00.000000+00:00' Pending (registry still resolving): summary: Select stayed pending after polling — sparse fields, auto-completes server-side value: request_id: 9d4f2c7a-1b3e-4a5c-8d6f-0e9a8b7c6d5e kyb_registry: uuid: 7c0a4f7e-2b1d-4f6a-9c3e-8d5b2a1f0e9d node_id: feature_kyb_registry status: Not Finished registry_status: null data_resolved: false company_name: Tesco PLC registration_number: 00445790 country_code: GB region: null company_type: null incorporation_date: null registered_address: null tax_number: null risk_level: null verification_status: null is_from_registry: true fetch_status: pending alternative_names: null nature_of_business: null registered_capital: null registered_capital_amount: null registered_capital_currency: null website: null email: null phone: null legal_entity_identifier: null location_of_registration: null financial_summary: {} officers: [] beneficial_owners: [] addresses: null industries: null accounts: null registry_data: {} user_provided_data: null confirmed_by_user_at: null last_console_edit_at: null is_editable: true vendor_data: company-1234 metadata: null created_at: '2026-06-11T10:45:00.000000+00:00' '400': description: Validation error. Field-level problems use DRF's standard envelope. content: application/json: examples: Missing kyb_response_id: summary: '`kyb_response_id` is required' value: kyb_response_id: - This field is required. save_api_request=false rejected: summary: Selects are always persisted value: save_api_request: - KYB registry select must be saved so billing and retrieval can be tracked. '403': description: 'Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — and also when the calling organization''s balance cannot cover the call. Authentication failures return `403` with `{"detail": ...}`; this API never returns `401`. Credit shortfalls return `403` with `{"error": ...}` before any screening or provider call happens.' content: application/json: examples: Missing or invalid API key: summary: No `x-api-key` header, or the key is invalid/revoked value: detail: You do not have permission to perform this action. Not enough credits: summary: Organization balance cannot cover the call value: error: You don't have enough credits to perform this request. Please top up at https://business.didit.me '429': description: Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers. content: application/json: examples: Write rate limit exceeded: summary: More than 300 write requests in the current minute value: detail: Write request rate limit exceeded. You can make up to 300 requests per minute. schema: type: object properties: detail: type: string /v3/passive-liveness/: post: summary: Passive Liveness description: 'Detect presentation attacks — printed photos, screen replays, masks, deepfakes — from a single face image. No video, motion, or user interaction required. **How it works.** The image is analyzed by a biometric model; the largest detected face is evaluated and a liveness `score` (0–100) is computed. `status` is `Declined` when any `error`-severity warning fires: - `NO_FACE_DETECTED` — the liveness model found no face, - `LOW_LIVENESS_SCORE` — score at or below `face_liveness_score_decline_threshold` (default `30`), - `LIVENESS_FACE_ATTACK` — the model detected a presentation attack, - `FACE_IN_BLOCKLIST` / `POSSIBLE_FACE_IN_BLOCKLIST` — the face matches an entry on your face blocklist. `MULTIPLE_FACES_DETECTED` is a `warning`-severity signal (does not decline), and `DUPLICATED_FACE` / `POSSIBLE_DUPLICATED_FACE` are `information`-severity signals that the same face already appeared in another approved session (they never decline). `face_quality` and `face_luminance` (0–100, nullable) describe capture quality. **Blocklist and duplicate screening.** Runs on **every** call — with or without `save_api_request` — by matching the detected face against your application''s face search index. Prior faces with the same `vendor_data` are excluded from the entire index search — so re-running liveness for the same user does not trigger `DUPLICATED_FACE`, and a blocklisted face originating from a session with the caller''s own `vendor_data` is also NOT flagged (`FACE_IN_BLOCKLIST` screening is bypassed for same-`vendor_data` faces). **Billing.** Each `200` response consumes one Passive Liveness API credit (standalone APIs have no free tier). Insufficient balance returns `403` before any image processing. **Session persistence and face enrollment (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session (Business Console, `GET /v3/session/{sessionId}/decision/` via the returned `request_id`, `status.updated` webhook), the reference image and face crop are stored, and the detected face is **enrolled into your face search index** so later Face Search calls and duplicate checks can match it. When `false`, the call is one-shot: nothing is stored and the face is not enrolled (blocklist/duplicate screening still runs). **Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload, no session is persisted, and no credits are consumed. **Authentication.** Send your application''s API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`) — this API never returns `401`. **Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.' operationId: post_v3passive-liveness tags: - Standalone APIs parameters: [] requestBody: required: true content: multipart/form-data: schema: type: object required: - user_image properties: user_image: type: string format: binary description: 'Front-facing face image (e.g. a selfie). Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `webp`. Maximum upload size: **5 MB** (larger files are rejected with `400`). Images are automatically compressed to ~0.5 MB before processing, so very high resolutions do not improve accuracy. The image should contain a single subject — when several faces are present, the largest one is evaluated and a `MULTIPLE_FACES_DETECTED` warning is added.' face_liveness_score_decline_threshold: type: number format: float default: 30 minimum: 0 maximum: 100 description: Liveness score threshold (0–100). A computed score **at or below** this value emits a `LOW_LIVENESS_SCORE` warning and sets `status` to `Declined`. Default `30`; raise it for a stricter anti-spoofing posture. Values outside 0–100 return `400`. example: 30 rotate_image: type: boolean default: false description: When `true`, the service tries 90-degree rotations of the input and uses the orientation that yields the best face detection. Useful when EXIF orientation is missing. Adds latency. example: false save_api_request: type: boolean default: true description: When `true` (default), persists the call as an API-type session, stores the face images, emits a `status.updated` webhook, and enrolls the detected face into your face search index. When `false`, the call is one-shot — nothing is stored and the face is not enrolled. Blocklist and duplicate screening run either way. example: true vendor_data: type: string description: Optional opaque identifier (your user id, email, UUID…) stored on the persisted session and echoed back. Also used to exclude prior sessions with the same `vendor_data` from the duplicate-face check, so re-verifying the same user does not raise `DUPLICATED_FACE`. example: user-123 metadata: type: object additionalProperties: true description: Optional JSON object stored with the session (when `save_api_request=true`) and echoed back. In multipart requests, send it as a JSON-encoded string field — it is parsed into an object. example: flow: withdrawal example: user_image: (binary JPEG/PNG selfie) face_liveness_score_decline_threshold: 30 rotate_image: false save_api_request: true vendor_data: user-123 metadata: flow: withdrawal responses: '200': description: Passive liveness completed. `liveness.score` is the model's 0–100 confidence that the image shows a live person; `status` is `Approved` unless an `error`-severity warning fired. A spoofed or blocklisted face still returns `200` — inspect `liveness.status` and `liveness.warnings`, not just the HTTP code. When `save_api_request=true`, `request_id` is the persisted session id. content: application/json: examples: Approved: summary: Live face — no warnings value: request_id: a1b2c3d4-e5f6-7890-1234-567890abcdef liveness: status: Approved method: PASSIVE score: 95 user_image: entities: - bbox: - 661 - 728 - 1688 - 2188 confidence: 0.732973 age: 26.91 gender: male race: null best_angle: 0 warnings: [] face_quality: 84.21 face_luminance: 50.33 vendor_data: user-123 metadata: null created_at: '2026-06-12T01:04:42.763237+00:00' Declined - presentation attack: summary: The model flagged a spoof (print, replay, mask…) value: request_id: f1c8c9b2-d3e4-4f5a-9b8c-7d6e5f4a3b2c liveness: status: Declined method: PASSIVE score: 12.5 user_image: entities: - bbox: - 100 - 120 - 420 - 540 confidence: 0.81 age: 30.1 gender: female race: null best_angle: 0 warnings: - risk: LOW_LIVENESS_SCORE feature: LIVENESS additional_data: null log_type: error short_description: Low liveness score long_description: The liveness check resulted in a low score, indicating potential use of non-live facial representations or poor-quality biometric data. - risk: LIVENESS_FACE_ATTACK feature: LIVENESS additional_data: null log_type: error short_description: Liveness Face Attack long_description: The system detected a potential attempt to bypass the liveness check. face_quality: 72.4 face_luminance: 48.2 vendor_data: user-123 metadata: null created_at: '2026-06-12T01:04:42.763237+00:00' Declined - face on blocklist: summary: Live face, but it matches a blocklist entry value: request_id: 0b54a1de-22aa-4e0f-9c84-5a2b1f3c4d5e liveness: status: Declined method: PASSIVE score: 93.75 user_image: entities: - bbox: - 40 - 40 - 100 - 100 confidence: 0.722082 age: 27 gender: male race: null best_angle: 0 warnings: - risk: FACE_IN_BLOCKLIST feature: LIVENESS additional_data: blocklisted_session_id: 882c42d5-8a4d-4d20-8080-a22f57822c86 blocklisted_session_number: 3242 api_service: null log_type: error short_description: Face in blocklist long_description: The system identified a face in the blocklist, which means the face is not allowed to be verified. face_quality: null face_luminance: null vendor_data: user-123 metadata: null created_at: '2026-06-12T01:04:42.763237+00:00' schema: type: object properties: request_id: type: string format: uuid description: Persisted session id when `save_api_request=true` (usable with `GET /v3/session/{sessionId}/decision/`); otherwise a transient correlation UUID. liveness: type: object properties: status: type: string enum: - Approved - Declined description: '`Declined` when at least one `error`-severity warning fired (no face, low score, presentation attack, or a blocklist hit); `Approved` otherwise. `warning`/`information` entries (multiple faces, duplicates) never decline on their own.' method: type: string enum: - PASSIVE description: Always `PASSIVE` for this endpoint. score: type: number format: float nullable: true minimum: 0 maximum: 100 description: Liveness confidence (0–100, two decimals) that the image depicts a live person rather than a spoof. `null` when the model returned no confidence. example: 95 user_image: type: object description: Face-detection results for the submitted image. properties: entities: type: array items: type: object properties: bbox: type: array items: type: integer minItems: 4 maxItems: 4 description: Bounding box of the detected face as `[x_min, y_min, x_max, y_max]` pixel coordinates in the processed image. example: - 661 - 728 - 1688 - 2188 confidence: type: number format: float minimum: 0 maximum: 1 description: Face-detection confidence (0–1). example: 0.732973 age: type: number format: float description: Model-estimated age of the detected face, in years. Informational only — it does not affect the liveness decision. example: 26.91 gender: type: string description: Model-predicted gender of the detected face (`male` or `female`). Informational only. example: male race: type: string nullable: true description: Reserved field — always `null` in responses from this endpoint. example: null description: One entry per detected face. The largest face is the one evaluated. best_angle: type: integer nullable: true description: Rotation (degrees) that produced the best face detection; non-zero only when `rotate_image=true` corrected the orientation. example: 0 warnings: type: array description: 'Risk signals. `error` severity (declines): `NO_FACE_DETECTED`, `LOW_LIVENESS_SCORE` (score at or below the threshold), `LIVENESS_FACE_ATTACK`, `FACE_IN_BLOCKLIST`, `POSSIBLE_FACE_IN_BLOCKLIST`. `warning` severity (review, no decline): `MULTIPLE_FACES_DETECTED`. `information` severity (no decline): `DUPLICATED_FACE`, `POSSIBLE_DUPLICATED_FACE` — the same face already appeared in another approved session.' items: type: object properties: risk: type: string enum: - NO_FACE_DETECTED - LOW_LIVENESS_SCORE - LIVENESS_FACE_ATTACK - MULTIPLE_FACES_DETECTED - FACE_IN_BLOCKLIST - POSSIBLE_FACE_IN_BLOCKLIST - DUPLICATED_FACE - POSSIBLE_DUPLICATED_FACE description: Machine-readable risk code. feature: type: string enum: - LIVENESS description: Feature that raised the warning. Always `LIVENESS` on this endpoint. additional_data: type: object nullable: true additionalProperties: true description: '`null` for most warnings. Blocklist hits carry `{blocklisted_session_id, blocklisted_session_number, api_service}`; duplicate hits carry `{duplicated_session_id, duplicated_session_number, api_service}` pointing at the matching session.' log_type: type: string enum: - error - warning - information description: Severity. `error` warnings drive `status` to `Declined`; `warning` and `information` entries are advisory and never decline on their own. short_description: type: string description: Human-readable one-line summary of the risk. long_description: type: string description: Human-readable explanation of the risk. face_quality: type: number format: float nullable: true minimum: 0 maximum: 100 description: Capture-quality score of the detected face, normalized to 0–100. `null` when the model could not produce a reliable value. example: 84.21 face_luminance: type: number format: float nullable: true minimum: 0 maximum: 100 description: Brightness of the detected face, normalized to 0–100. `null` when unavailable. example: 50.33 vendor_data: type: string nullable: true description: Echo of the `vendor_data` you sent, or `null`. metadata: type: object nullable: true additionalProperties: true description: Echo of the `metadata` object you sent, or `null`. created_at: type: string format: date-time description: ISO 8601 timestamp (UTC) of when the response was generated, e.g. `2026-06-12T01:04:42.763237+00:00`. '400': description: 'Validation error. Returned when `user_image` is missing, exceeds 5 MB, has an unsupported extension, fails server-side image decoding, or when an option is out of range. Field errors use DRF''s `{field: [messages]}` envelope; image-decoding failures use `{"error": ...}`.' content: application/json: examples: Missing image: summary: '`user_image` not included in the form data' value: user_image: - No file was submitted. Invalid image format: summary: Allowed extension but undecodable image content value: error: Invalid user image format. Unsupported file extension: summary: File extension outside tiff/jpg/jpeg/png/webp value: user_image: - 'File extension “txt” is not allowed. Allowed extensions are: tiff, jpg, jpeg, png, webp.' File too large: summary: Upload exceeds the 5 MB limit value: user_image: - File size should not exceed 5 MB '403': description: 'Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — and also when the calling organization''s balance cannot cover the call. Authentication failures return `403` with `{"detail": ...}`; this API never returns `401`. Credit shortfalls return `403` with `{"error": ...}` before any image processing happens.' content: application/json: examples: Missing or invalid API key: summary: No `x-api-key` header, or the key is invalid/revoked value: detail: You do not have permission to perform this action. Not enough credits: summary: Organization balance cannot cover the call value: error: You don't have enough credits to perform this request. Please top up at https://business.didit.me '429': description: Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers. content: application/json: examples: Write rate limit exceeded: summary: More than 300 write requests in the current minute value: detail: Write request rate limit exceeded. You can make up to 300 requests per minute. schema: type: object properties: detail: type: string security: - ApiKeyAuth: [] x-codeSamples: - lang: curl label: cURL source: "curl -X POST 'https://verification.didit.me/v3/passive-liveness/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -F 'user_image=@./selfie.jpg' \\\n -F 'face_liveness_score_decline_threshold=30' \\\n -F 'save_api_request=true' \\\n -F 'vendor_data=user-123'" - lang: python label: Python source: "import requests\n\nurl = 'https://verification.didit.me/v3/passive-liveness/'\nheaders = {'x-api-key': 'YOUR_API_KEY'}\n\nwith open('selfie.jpg', 'rb') as f:\n files = {'user_image': ('selfie.jpg', f, 'image/jpeg')}\n data = {\n 'face_liveness_score_decline_threshold': 30,\n 'save_api_request': 'true',\n 'vendor_data': 'user-123',\n }\n resp = requests.post(url, headers=headers, files=files, data=data, timeout=60)\n\nresp.raise_for_status()\nbody = resp.json()\nprint('status:', body['liveness']['status'])\nprint('score:', body['liveness']['score'])\nfor w in body['liveness']['warnings']:\n print('warning:', w['risk'], '-', w['log_type'])" - lang: javascript label: JavaScript source: "import fs from 'node:fs';\n\nconst form = new FormData();\nform.append('user_image', new Blob([fs.readFileSync('./selfie.jpg')]), 'selfie.jpg');\nform.append('face_liveness_score_decline_threshold', '30');\nform.append('save_api_request', 'true');\nform.append('vendor_data', 'user-123');\n\nconst response = await fetch('https://verification.didit.me/v3/passive-liveness/', {\n method: 'POST',\n headers: { 'x-api-key': 'YOUR_API_KEY' },\n body: form,\n});\n\nif (!response.ok) throw new Error(`Passive liveness failed: ${response.status}`);\nconst body = await response.json();\nconsole.log('status:', body.liveness.status, 'score:', body.liveness.score);\nbody.liveness.warnings.forEach(w => console.log('warning:', w.risk, '-', w.log_type));" /v3/database-validation/: post: summary: Database Validation (standalone) operationId: post_v3database-validation tags: - Standalone APIs security: - ApiKeyAuth: [] parameters: [] description: 'Validate a person''s identity data against official government and registry sources — CPF in Brazil, RENAPER in Argentina, DNI registries in Spain and Peru, INE in Mexico, and 100+ more services across 60+ countries. **Service selection.** Each country exposes one or more catalog services identified by a `service_id` (e.g. `bra_cpf`, `arg_renaper`, `pan_cedula_sib`). Pin the services you want with the `services` array — if you omit it, exactly **one** default service runs (the longest-established live service for the country). Sending `services` also switches the response to the extended shape with `services_used` and `match_score`. Discover services, their required fields, and prices via `GET /v1/organization/database-validation-countries/` (a catalog endpoint not documented in this spec) or the Business Console workflow editor. Some services are flagged `requires_consent=true` in the catalog and demand `consent=true`; others require onboarding for your organization before they can be called. **Input fields.** `identification_number` is a universal field that maps to the right country-specific field automatically (see its description). Country format rules are enforced before any provider is called (e.g. BRA CPF must be exactly 11 digits) and format failures are never billed. Biometric services need a `selfie` upload (multipart only); Argentina RENAPER additionally requires `gender`. **Results.** Each service returns a field-by-field `validation` map plus a vendor-neutral `outcome_code` (`MATCH`, `PARTIAL_MATCH`, `NO_MATCH`, `DOCUMENT_NOT_FOUND`, `INVALID_DOCUMENT_FORMAT`, `INVALID_INPUT`, `MINOR_BLOCKED`, `DECEASED`, `BIOMETRIC_NO_MATCH`, `BIOMETRIC_IMAGE_UNUSABLE`, `INCONCLUSIVE`, `REGISTRY_UNAVAILABLE`, `REGISTRY_ERROR`). Note the distinctions: `NO_MATCH` is a definitive mismatch, `INCONCLUSIVE` means the registry could not confirm either way (review, not decline), and `BIOMETRIC_IMAGE_UNUSABLE` is a technical selfie problem (retake), unlike the definitive `BIOMETRIC_NO_MATCH`. The overall `match_type` aggregates across services (any full → `full_match`, else any partial → `partial_match`, else `no_match`) and `no_match_action`/`partial_match_action` translate it into the final `status`. `validation_type` is derived: `two_by_two` when two or more distinct services produced a full match (on the identification number, on full name + date of birth together, or on the address without contradictions), else `one_by_one`. **Persistence.** `save_api_request` defaults to **true**: the result is stored as a session (`request_id` works with `GET /v3/session/{sessionId}/decision/`), appears in the console, and fires a `status.updated` webhook. It also controls the `validations` shape — per-service objects when saved, a merged `{field: match}` map when not. **Billing.** Per-service: each service that returns a billable result is charged its own catalog price. Failed or skipped services are not billed, and requests rejected with `400` cost nothing. The pre-flight balance check covers the sum of the selected services'' prices (`403` when short). **Failures.** When *no* selected service returns a usable result the API responds `502` with a `validation_errors` array; nothing is billed (a session is still recorded with status `Not Finished` when `save_api_request=true`). When only some services fail, the `200` response carries their failures in `database_validation.errors`. **Sandbox.** Keys from sandbox applications skip the registry calls and billing and return a static `full_match` response — the sandbox payload always includes `services_used` and `match_score: 100` regardless of how `services` was sent, so don''t validate the live extended-shape gating or the 0.0–1.0 `match_score` fraction against sandbox responses. Send the request as `application/json`, or as `multipart/form-data` when uploading a `selfie`.' requestBody: required: true content: application/json: schema: type: object required: - issuing_state properties: issuing_state: type: string description: ISO 3166-1 **alpha-3** country code of the registry to validate against (e.g. `BRA`, `ESP`, `COL`). Determines which services are available. Unsupported codes return `400` with the full list of valid options. example: BRA services: type: array items: type: string description: Catalog `service_id`s to run for this country (e.g. `["bra_cpf"]`). Also accepts a single string or a comma-separated/JSON-encoded string. **If omitted or empty, exactly one default service runs** — the longest-established live service for the country; newer, biometric, and pay-as-you-go services must be named explicitly. Sending `services` switches the response to the extended shape (adds `services_used` and `match_score`). Every id must exist and be live for the `issuing_state`; services that require onboarding return `400` until activated for your organization. Discover available services per country via `GET /v1/organization/database-validation-countries/` (a catalog endpoint not documented in this spec) or the Business Console workflow editor. example: - bra_cpf validation_type: type: string deprecated: true description: DEPRECATED. Accepted for backward compatibility but ignored — the response `validation_type` is now derived from how many services full-matched. Use `services` to pin specific services. consent: type: boolean default: false description: Set to `true` when the end user has explicitly consented to the selected validation services. Required for services flagged `requires_consent=true` in the catalog — without it those services return `400` with `services_requiring_consent`. identification_number: type: string description: 'Universal identification number — automatically mapped to the correct country-specific field, so you can use it instead of `personal_number`/`tax_number`/`document_number`: ARG→document_number (DNI), BOL→document_number (CI), BRA→tax_number (CPF, 11 digits), CHL→personal_number (RUT), COL→personal_number (Cédula), CRI→personal_number (Cédula), DOM→personal_number (Cédula, 11 digits), ECU→personal_number (Cédula, 10 digits), ESP→personal_number (DNI/NIE), GTM→document_number (DPI), HND→document_number (DNI), MEX→personal_number (CURP, 18 chars), PAN→personal_number (Cédula), PER→personal_number (DNI, 8 digits), PRY→document_number (CI), SLV→document_number (DUI), URY→personal_number (CI), VEN→document_number (Cédula). It never overrides an explicitly provided country-specific field.' first_name: type: string description: The individual's first name. Required by some services/countries. example: John last_name: type: string description: The individual's last name. Required by some services/countries. example: Doe middle_name: type: string description: Middle name, used by some country services (AUS, NZL, …). full_name: type: string description: Full name — some services accept this in lieu of first/last name (e.g. CHN services, which match the native-script name). date_of_birth: type: string format: date description: Date of birth, `YYYY-MM-DD`. Required by many services. example: '1980-01-01' personal_number: type: string description: 'Government-issued unique personal identifier. Used by: CHL (RUT), COL (Cédula), CRI, DOM, ECU, ESP (DNI/NIE), MEX (CURP), PAN (Cédula), PER (DNI), URY. Consider `identification_number` instead.' tax_number: type: string description: 'Tax identification number. Used by: BRA (CPF, 11 digits). Consider `identification_number` instead.' document_number: type: string description: 'Document number. Used by: ARG (DNI), BOL (CI), GTM (DPI), HND (DNI), PRY (CI), SLV (DUI), VEN (Cédula). Consider `identification_number` instead.' document_type: type: string enum: - P - DL - ID - RP - SSC - HIC - WP - TC - VISA - PSC - BC - OTHER description: 'Type of document being validated: `P` passport, `DL` driver license, `ID` national ID, `RP` residence permit (plus `SSC`, `HIC`, `WP`, `TC`, `VISA`, `PSC`, `BC`, `OTHER`). Required by some services (e.g. ESP).' expiration_date: type: string format: date description: Document expiration date, `YYYY-MM-DD`. Required for ESP (Spain) validation. example: '2030-01-15' date_of_issue: type: string format: date description: Document issue date, `YYYY-MM-DD` (fecha de expedición). Required for COL (Colombia) cédula validation. example: '2015-06-20' nationality: type: string description: Nationality as ISO 3166-1 alpha-3. Required by some services. gender: type: string enum: - M - F - X description: 'Gender: `M`, `F`, or `X` (other/unknown). Required for Argentina''s RENAPER validation (`arg_renaper`).' address: description: Residential address. Prefer the structured object `{"street_1":"123 Main St","street_2":"Apt 4B","city":"Springfield","region":"IL","postal_code":"62701","country":"US"}`; a complete single-line string is still accepted. Required by address-verification services; when a service defines address requirements, missing parts return `400` with `address_fields_required_by`. oneOf: - type: object properties: street_1: type: string street_2: type: string city: type: string region: type: string postal_code: type: string country: type: string - type: string description: Complete single-line address (legacy). address_element_1: type: string description: Street address including street number and type. If omitted, derived from `address`. address_element_2: type: string description: Apartment, unit, building, floor, or extra address line. Only send when you have it explicitly. address_element_3: type: string description: City, suburb, district, locality, or neighborhood. If omitted, derived from `address`. address_element_4: type: string description: State, province, region, or town. If omitted, derived from `address`. address_element_5: type: string description: Postcode or postal code. `postal_code` is accepted as an alias. postal_code: type: string description: Postal code for address-based services (alias of `address_element_5`). driver_license_number: type: string description: Driver licence number for government document-verification services (AUS, NZL, IND). driver_license_card_number: type: string description: Physical driver licence card number, where required separately from the licence number (AUS). driver_license_state: type: string description: Driver licence state or territory of issue (AUS). driver_license_version: type: string description: Driver licence version code (NZL). passport_number: type: string description: Passport number for government passport-verification services (AUS, NZL, IND, CHN). passport_expiration_date: type: string format: date description: Passport expiry date, `YYYY-MM-DD`, for passport-verification services. passport_file_number: type: string description: Passport file number (IND). passport_issue_country: type: string description: Issuing country of the passport (ISO 3166-1 alpha-3) for passport-verification services. medicare_card_number: type: string description: Medicare card number (AUS). immi_card_number: type: string description: Australian ImmiCard number. immi_card_expiry_date: type: string format: date description: Australian ImmiCard expiry date. citizenship_certificate_number: type: string description: Citizenship certificate number (AUS). birth_registration_number: type: string description: Birth-certificate registration number (AUS). birth_registration_date: type: string format: date description: Birth-certificate registration date (AUS). birth_registration_state: type: string description: Birth-certificate registration state (AUS). marriage_certificate_number: type: string description: Marriage certificate number (AUS). change_of_name_certificate_number: type: string description: Change-of-name certificate number (AUS). first_partner_name: type: string description: First name of the partner on a marriage certificate (AUS). last_partner_name: type: string description: Last name of the partner on a marriage certificate (AUS). voter_id: type: string description: Voter registration number (IND EPIC, IRL). epic_card: type: string description: India EPIC voter card number. pan: type: string description: India PAN (Permanent Account Number). national_id: type: string description: National ID number (CHN, MYS, KEN, KHM, NGA, ZAF, …). cic: type: string description: Mexican INE/IFE Código de Identificación de Credencial (CIC), 9 digits. Used by the MEX INE credential-validity service (`mex_ine_vigencia`). identificador_ciudadano: type: string description: Mexican INE Identificador del Ciudadano (9 digits). Used with `cic` for modern INE models (E/F/G/H) in `mex_ine_vigencia`. ocr: type: string description: Mexican INE OCR number (13 digits, back of card). Used with `cic` for Model D in `mex_ine_vigencia`. voter_number: type: string description: Mexican INE Clave de Elector (18 chars). Used with `emission_number` for legacy IFE models (A/B/C) in `mex_ine_vigencia`. emission_number: type: string description: Mexican INE Número de Emisión. Used with `voter_number` for legacy IFE models (A/B/C) in `mex_ine_vigencia`. bvn: type: string description: Nigerian Bank Verification Number. bank_card_number: type: string description: Bank card number (CHN bank-card verification). ssn: type: string description: Social Security Number (USA). phone: type: string description: Phone number for phone-verification services. landline: type: string description: Landline number for phone-verification services. email: type: string format: email description: Email address for identity-verification services. partial_match_action: type: string enum: - DECLINE - NO_ACTION default: NO_ACTION description: What a `partial_match` does to `database_validation.status`. Defaults to `NO_ACTION` (status stays `Approved`, with a warning). no_match_action: type: string enum: - DECLINE - NO_ACTION default: DECLINE description: What a `no_match` does to `database_validation.status`. Defaults to `DECLINE`. save_api_request: type: boolean default: true description: 'Persist the validation as a session (console visibility, decision endpoint, `status.updated` webhook). Also changes the `validations` response shape: per-service objects when `true` (default), a merged field map when `false`.' vendor_data: type: string description: Your identifier for the validated user; echoed back and stored with the session. metadata: type: object nullable: true description: Free-form JSON stored with the request and echoed back. multipart/form-data: schema: type: object required: - issuing_state properties: issuing_state: type: string description: ISO 3166-1 **alpha-3** country code of the registry to validate against (e.g. `BRA`, `ESP`, `COL`). Determines which services are available. Unsupported codes return `400` with the full list of valid options. example: BRA services: type: array items: type: string description: Catalog `service_id`s to run for this country (e.g. `["bra_cpf"]`). Also accepts a single string or a comma-separated/JSON-encoded string. **If omitted or empty, exactly one default service runs** — the longest-established live service for the country; newer, biometric, and pay-as-you-go services must be named explicitly. Sending `services` switches the response to the extended shape (adds `services_used` and `match_score`). Every id must exist and be live for the `issuing_state`; services that require onboarding return `400` until activated for your organization. Discover available services per country via `GET /v1/organization/database-validation-countries/` (a catalog endpoint not documented in this spec) or the Business Console workflow editor. example: - bra_cpf validation_type: type: string deprecated: true description: DEPRECATED. Accepted for backward compatibility but ignored — the response `validation_type` is now derived from how many services full-matched. Use `services` to pin specific services. consent: type: boolean default: false description: Set to `true` when the end user has explicitly consented to the selected validation services. Required for services flagged `requires_consent=true` in the catalog — without it those services return `400` with `services_requiring_consent`. identification_number: type: string description: 'Universal identification number — automatically mapped to the correct country-specific field, so you can use it instead of `personal_number`/`tax_number`/`document_number`: ARG→document_number (DNI), BOL→document_number (CI), BRA→tax_number (CPF, 11 digits), CHL→personal_number (RUT), COL→personal_number (Cédula), CRI→personal_number (Cédula), DOM→personal_number (Cédula, 11 digits), ECU→personal_number (Cédula, 10 digits), ESP→personal_number (DNI/NIE), GTM→document_number (DPI), HND→document_number (DNI), MEX→personal_number (CURP, 18 chars), PAN→personal_number (Cédula), PER→personal_number (DNI, 8 digits), PRY→document_number (CI), SLV→document_number (DUI), URY→personal_number (CI), VEN→document_number (Cédula). It never overrides an explicitly provided country-specific field.' first_name: type: string description: The individual's first name. Required by some services/countries. example: John last_name: type: string description: The individual's last name. Required by some services/countries. example: Doe middle_name: type: string description: Middle name, used by some country services (AUS, NZL, …). full_name: type: string description: Full name — some services accept this in lieu of first/last name (e.g. CHN services, which match the native-script name). date_of_birth: type: string format: date description: Date of birth, `YYYY-MM-DD`. Required by many services. example: '1980-01-01' personal_number: type: string description: 'Government-issued unique personal identifier. Used by: CHL (RUT), COL (Cédula), CRI, DOM, ECU, ESP (DNI/NIE), MEX (CURP), PAN (Cédula), PER (DNI), URY. Consider `identification_number` instead.' tax_number: type: string description: 'Tax identification number. Used by: BRA (CPF, 11 digits). Consider `identification_number` instead.' document_number: type: string description: 'Document number. Used by: ARG (DNI), BOL (CI), GTM (DPI), HND (DNI), PRY (CI), SLV (DUI), VEN (Cédula). Consider `identification_number` instead.' document_type: type: string enum: - P - DL - ID - RP - SSC - HIC - WP - TC - VISA - PSC - BC - OTHER description: 'Type of document being validated: `P` passport, `DL` driver license, `ID` national ID, `RP` residence permit (plus `SSC`, `HIC`, `WP`, `TC`, `VISA`, `PSC`, `BC`, `OTHER`). Required by some services (e.g. ESP).' expiration_date: type: string format: date description: Document expiration date, `YYYY-MM-DD`. Required for ESP (Spain) validation. example: '2030-01-15' date_of_issue: type: string format: date description: Document issue date, `YYYY-MM-DD` (fecha de expedición). Required for COL (Colombia) cédula validation. example: '2015-06-20' nationality: type: string description: Nationality as ISO 3166-1 alpha-3. Required by some services. gender: type: string enum: - M - F - X description: 'Gender: `M`, `F`, or `X` (other/unknown). Required for Argentina''s RENAPER validation (`arg_renaper`).' address: description: Residential address. Prefer the structured object `{"street_1":"123 Main St","street_2":"Apt 4B","city":"Springfield","region":"IL","postal_code":"62701","country":"US"}`; a complete single-line string is still accepted. Required by address-verification services; when a service defines address requirements, missing parts return `400` with `address_fields_required_by`. oneOf: - type: object properties: street_1: type: string street_2: type: string city: type: string region: type: string postal_code: type: string country: type: string - type: string description: Complete single-line address (legacy). address_element_1: type: string description: Street address including street number and type. If omitted, derived from `address`. address_element_2: type: string description: Apartment, unit, building, floor, or extra address line. Only send when you have it explicitly. address_element_3: type: string description: City, suburb, district, locality, or neighborhood. If omitted, derived from `address`. address_element_4: type: string description: State, province, region, or town. If omitted, derived from `address`. address_element_5: type: string description: Postcode or postal code. `postal_code` is accepted as an alias. postal_code: type: string description: Postal code for address-based services (alias of `address_element_5`). driver_license_number: type: string description: Driver licence number for government document-verification services (AUS, NZL, IND). driver_license_card_number: type: string description: Physical driver licence card number, where required separately from the licence number (AUS). driver_license_state: type: string description: Driver licence state or territory of issue (AUS). driver_license_version: type: string description: Driver licence version code (NZL). passport_number: type: string description: Passport number for government passport-verification services (AUS, NZL, IND, CHN). passport_expiration_date: type: string format: date description: Passport expiry date, `YYYY-MM-DD`, for passport-verification services. passport_file_number: type: string description: Passport file number (IND). passport_issue_country: type: string description: Issuing country of the passport (ISO 3166-1 alpha-3) for passport-verification services. medicare_card_number: type: string description: Medicare card number (AUS). immi_card_number: type: string description: Australian ImmiCard number. immi_card_expiry_date: type: string format: date description: Australian ImmiCard expiry date. citizenship_certificate_number: type: string description: Citizenship certificate number (AUS). birth_registration_number: type: string description: Birth-certificate registration number (AUS). birth_registration_date: type: string format: date description: Birth-certificate registration date (AUS). birth_registration_state: type: string description: Birth-certificate registration state (AUS). marriage_certificate_number: type: string description: Marriage certificate number (AUS). change_of_name_certificate_number: type: string description: Change-of-name certificate number (AUS). first_partner_name: type: string description: First name of the partner on a marriage certificate (AUS). last_partner_name: type: string description: Last name of the partner on a marriage certificate (AUS). voter_id: type: string description: Voter registration number (IND EPIC, IRL). epic_card: type: string description: India EPIC voter card number. pan: type: string description: India PAN (Permanent Account Number). national_id: type: string description: National ID number (CHN, MYS, KEN, KHM, NGA, ZAF, …). cic: type: string description: Mexican INE/IFE Código de Identificación de Credencial (CIC), 9 digits. Used by the MEX INE credential-validity service (`mex_ine_vigencia`). identificador_ciudadano: type: string description: Mexican INE Identificador del Ciudadano (9 digits). Used with `cic` for modern INE models (E/F/G/H) in `mex_ine_vigencia`. ocr: type: string description: Mexican INE OCR number (13 digits, back of card). Used with `cic` for Model D in `mex_ine_vigencia`. voter_number: type: string description: Mexican INE Clave de Elector (18 chars). Used with `emission_number` for legacy IFE models (A/B/C) in `mex_ine_vigencia`. emission_number: type: string description: Mexican INE Número de Emisión. Used with `voter_number` for legacy IFE models (A/B/C) in `mex_ine_vigencia`. bvn: type: string description: Nigerian Bank Verification Number. bank_card_number: type: string description: Bank card number (CHN bank-card verification). ssn: type: string description: Social Security Number (USA). phone: type: string description: Phone number for phone-verification services. landline: type: string description: Landline number for phone-verification services. email: type: string format: email description: Email address for identity-verification services. partial_match_action: type: string enum: - DECLINE - NO_ACTION default: NO_ACTION description: What a `partial_match` does to `database_validation.status`. Defaults to `NO_ACTION` (status stays `Approved`, with a warning). no_match_action: type: string enum: - DECLINE - NO_ACTION default: DECLINE description: What a `no_match` does to `database_validation.status`. Defaults to `DECLINE`. save_api_request: type: boolean default: true description: 'Persist the validation as a session (console visibility, decision endpoint, `status.updated` webhook). Also changes the `validations` response shape: per-service objects when `true` (default), a merged field map when `false`.' vendor_data: type: string description: Your identifier for the validated user; echoed back and stored with the session. metadata: type: object nullable: true description: Free-form JSON stored with the request and echoed back. selfie: type: string format: binary description: Selfie image (`jpg`, `jpeg`, `png`, `webp`; max 2 MB — images are compressed to ~1.5 MB before upload to the registry) for biometric validation services, e.g. Argentina RENAPER (`arg_renaper`) and Panama SIB (`pan_cedula_sib`, `pan_cedula_sib_plus`). Required whenever a selected service lists it as a required field. Only available via `multipart/form-data`. x-codeSamples: - lang: curl label: curl source: "curl -X POST https://verification.didit.me/v3/database-validation/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"issuing_state\": \"BRA\",\n \"services\": [\"bra_cpf\"],\n \"identification_number\": \"12345678900\",\n \"first_name\": \"John\",\n \"last_name\": \"Doe\",\n \"date_of_birth\": \"1980-01-01\",\n \"vendor_data\": \"user-1234\"\n }'" - lang: python label: Python source: "import os, requests\n\nresp = requests.post(\n \"https://verification.didit.me/v3/database-validation/\",\n headers={\"x-api-key\": os.environ[\"DIDIT_API_KEY\"]},\n json={\n \"issuing_state\": \"BRA\",\n \"services\": [\"bra_cpf\"],\n \"identification_number\": \"12345678900\",\n \"first_name\": \"John\",\n \"last_name\": \"Doe\",\n \"date_of_birth\": \"1980-01-01\",\n \"vendor_data\": \"user-1234\",\n },\n timeout=45,\n)\nresp.raise_for_status()\ndv = resp.json()[\"database_validation\"]\nprint(dv[\"status\"], dv[\"match_type\"]) # e.g. Approved full_match\n\n# Biometric services need a selfie via multipart/form-data:\n# requests.post(..., data={\"issuing_state\": \"ARG\", \"services\": '[\"arg_renaper\"]',\n# \"identification_number\": \"12345678\", \"gender\": \"M\", ...},\n# files={\"selfie\": open(\"selfie.jpg\", \"rb\")})" - lang: javascript label: JavaScript source: "const res = await fetch('https://verification.didit.me/v3/database-validation/', {\n method: 'POST',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n issuing_state: 'BRA',\n services: ['bra_cpf'],\n identification_number: '12345678900',\n first_name: 'John',\n last_name: 'Doe',\n date_of_birth: '1980-01-01',\n vendor_data: 'user-1234',\n }),\n});\nconst data = await res.json();\nconsole.log(data.database_validation.status, data.database_validation.match_type);" responses: '200': description: Validation completed — at least one selected service returned a usable result. Inspect `database_validation.match_type` for the aggregate outcome, `validations` for the per-service field comparisons and `outcome_code`s, and `errors` (when present) for services that failed. content: application/json: schema: type: object properties: request_id: type: string format: uuid description: Persisted session id when `save_api_request=true` (usable with `GET /v3/session/{sessionId}/decision/`); otherwise a transient correlation UUID. database_validation: type: object properties: status: type: string enum: - Approved - Declined description: '`Declined` when the overall `match_type` triggers a `DECLINE` action (`no_match_action` defaults to `DECLINE`, `partial_match_action` to `NO_ACTION`); `Approved` otherwise.' issuing_state: type: string description: Echo of the ISO 3166-1 alpha-3 country validated against. example: BRA validation_type: type: string enum: - one_by_one - two_by_two description: 'Derived from the results: `two_by_two` when ≥2 distinct services produced a full match (on the identification number, on full name + date of birth together, or on the address without contradictions); `one_by_one` otherwise. (The request field of the same name is deprecated and ignored.)' screened_data: type: object description: 'Echo of the input fields that were sent to the selected services (dates normalized to `YYYY-MM-DD`; a `selfie`, when sent, is returned as a pre-signed URL; `consent_obtained: true` is added when consent was given).' match_type: type: string nullable: true enum: - full_match - partial_match - no_match description: 'Overall result aggregated across services with field-level comparisons: any `full_match` → `full_match`; else any `partial_match` → `partial_match`; else `no_match`. Outcome-only services, such as biometric criminal screening, can return `null`; inspect each item in `validations` for its `outcome_code`.' validations: description: 'Shape depends on `save_api_request`. **`true` (default):** an array of per-service objects (`service_id`, `service_name`, `validation`, `outcome_code`, `outcome_detail`, `source_data`). **`false`:** a single flat object merging the field-level results of all services (`{"full_name": "full_match", ...}`).' oneOf: - type: array items: type: object description: Per-service validation result (returned when `save_api_request=true`, the default). properties: service_id: type: string description: Catalog id of the service that produced this result (e.g. `bra_cpf`). example: bra_cpf service_name: type: string description: Human-readable catalog name of the service. example: Brazil - CPF status check validation: type: object description: Field-by-field comparison between your input and the registry record. Keys are canonical field names (`full_name`, `date_of_birth`, `identification_number`, `address`, …); values are `full_match`, `partial_match`, or `no_match`. additionalProperties: type: string enum: - full_match - partial_match - no_match outcome_code: type: string enum: - MATCH - PARTIAL_MATCH - NO_MATCH - DOCUMENT_NOT_FOUND - INVALID_DOCUMENT_FORMAT - INVALID_INPUT - MINOR_BLOCKED - DECEASED - BIOMETRIC_NO_MATCH - BIOMETRIC_IMAGE_UNUSABLE - INCONCLUSIVE - REGISTRY_UNAVAILABLE - REGISTRY_ERROR description: Vendor-neutral outcome of the registry lookup. `MATCH`/`PARTIAL_MATCH`/`NO_MATCH` describe the comparison; `DOCUMENT_NOT_FOUND` means the identifier is not in the registry; `INCONCLUSIVE` means the registry could not confirm either way (routed to review — NOT a no-match); `BIOMETRIC_NO_MATCH` is a definitive face mismatch while `BIOMETRIC_IMAGE_UNUSABLE` is a technical image problem (retake the selfie); `DECEASED`/`MINOR_BLOCKED` are registry-policy outcomes; `REGISTRY_UNAVAILABLE`/`REGISTRY_ERROR` indicate upstream failures. outcome_detail: type: string nullable: true description: Raw upstream status detail backing the outcome code (e.g. an HTTP or registry status code). source_data: type: object nullable: true description: Cleaned, normalized view of the registry record using canonical field names (`identification_number`, `first_name`, `last_name`, `date_of_birth`, plus registry-specific flags). Image fields, when present, are returned as pre-signed URLs. - type: object additionalProperties: type: string enum: - full_match - partial_match - no_match description: Merged field-level results (stateless `save_api_request=false` shape). warnings: type: array items: type: object properties: feature: type: string description: Feature that raised the warning. risk: type: string description: Machine-readable risk identifier. additional_data: type: object nullable: true description: Extra context for the warning, when available. log_type: type: string description: '`warning`, `information`, or `error` — how the risk affected the outcome.' short_description: type: string description: One-line human-readable summary. long_description: type: string description: Full human-readable explanation. description: '`DATABASE_VALIDATION_NO_MATCH` or `DATABASE_VALIDATION_PARTIAL_MATCH` when applicable; empty on a full match.' errors: type: array description: Present only when some (but not all) selected services failed. Each entry carries `service_id`, `code`, and `message`. Failed services are not billed. items: type: object properties: service_id: type: string code: type: string example: empty_provider_response message: type: string services_used: type: array items: type: string description: Catalog ids of the services that returned a billable result. **Only present when the request explicitly sent `services`.** match_score: type: number description: Fraction (0.0–1.0) of attempted services that produced a full match (on the identification number, on full name + date of birth together, or on the address without contradictions). **Only present when the request explicitly sent `services`.** vendor_data: type: string nullable: true description: Echo of the `vendor_data` you sent. metadata: type: object nullable: true description: Echo of the `metadata` you sent. created_at: type: string format: date-time examples: Full match (Approved): summary: Default save_api_request=true — per-service `validations` objects value: request_id: 7f9a1c0e-1f2b-4f6e-9d3a-2b1c0e7f9a1c database_validation: status: Approved issuing_state: BRA validation_type: one_by_one screened_data: tax_number: '12345678900' first_name: John last_name: Doe date_of_birth: '1980-01-01' match_type: full_match validations: - validation: full_name: full_match date_of_birth: full_match identification_number: full_match outcome_code: MATCH outcome_detail: '200' service_id: bra_cpf service_name: Brazil - CPF status check source_data: identification_number: '12345678900' first_name: JOHN last_name: DOE date_of_birth: '1980-01-01' lgpd_minor: false minor_under_16: false minor_under_18: false warnings: [] vendor_data: user-1234 metadata: null created_at: '2026-06-11T10:30:00.000000+00:00' Explicit services (extended shape): summary: Request sent `services` — response adds `services_used` and `match_score` value: request_id: b3e7c1a2-9d4f-4f0b-8a6c-5e2d1f0a9b3e database_validation: status: Approved issuing_state: BRA validation_type: one_by_one screened_data: tax_number: '12345678900' first_name: John last_name: Doe date_of_birth: '1980-01-01' match_type: full_match validations: - validation: full_name: full_match date_of_birth: full_match identification_number: full_match outcome_code: MATCH outcome_detail: '200' service_id: bra_cpf service_name: Brazil - CPF status check source_data: identification_number: '12345678900' first_name: JOHN last_name: DOE date_of_birth: '1980-01-01' lgpd_minor: false minor_under_16: false minor_under_18: false warnings: [] services_used: - bra_cpf match_score: 1 vendor_data: user-1234 metadata: null created_at: '2026-06-11T10:30:00.000000+00:00' No match (Declined): summary: Registry record does not match — default `no_match_action=DECLINE` value: request_id: 0a1b2c3d-4e5f-6789-abcd-ef0123456789 database_validation: status: Declined issuing_state: BRA validation_type: one_by_one screened_data: tax_number: '12345678900' first_name: John last_name: Doe date_of_birth: '1980-01-01' match_type: no_match validations: - validation: full_name: no_match date_of_birth: no_match identification_number: no_match outcome_code: NO_MATCH outcome_detail: '200' service_id: bra_cpf service_name: Brazil - CPF status check source_data: identification_number: 00987654321 first_name: TOTALLY last_name: DIFFERENT NAME date_of_birth: '1999-01-01' lgpd_minor: false minor_under_16: false minor_under_18: false warnings: - feature: DATABASE_VALIDATION risk: DATABASE_VALIDATION_NO_MATCH additional_data: null log_type: error short_description: Database validation no match long_description: The system identified a no match in the database validation, requiring further investigation. vendor_data: null metadata: null created_at: '2026-06-11T10:31:00.000000+00:00' Stateless (save_api_request=false): summary: '`validations` collapses to a merged field map; nothing is stored' value: request_id: c4d5e6f7-8901-2345-6789-abcdef012345 database_validation: status: Approved issuing_state: BRA validation_type: one_by_one screened_data: tax_number: '12345678900' first_name: John last_name: Doe date_of_birth: '1980-01-01' match_type: full_match validations: full_name: full_match date_of_birth: full_match identification_number: full_match warnings: [] vendor_data: null metadata: null created_at: '2026-06-11T10:32:00.000000+00:00' '400': description: Validation error — nothing is billed and no provider is called. Field-level problems use DRF's standard envelope; catalog problems return a `services` array (plus `requires_onboarding` or `services_requiring_consent` when relevant); missing-field errors include `fields_required_by` mapping each missing field to the services that demand it; format problems checked per service return `invalid_fields_by_service`. content: application/json: examples: Invalid issuing_state: summary: Country not supported — message lists every valid alpha-3 code value: error: - 'Invalid issuing state. Valid options are: ARE, ARG, AUS, AUT, BEL, BGD, BOL, BRA, CAN, CHE, CHL, CHN, CIV, COL, CRI, CZE, DEU, DNK, DOM, ECU, ESP, FIN, FRA, GBR, GHA, GLB, GRC, GTM, HKG, HND, IDN, IND, IRL, ITA, KEN, KHM, MAR, MEX, MYS, NGA, NLD, NOR, NZL, OMN, PAN, PER, PHL, POL, PRT, PRY, QAT, SGP, SLV, SVK, SWE, THA, UGA, URY, USA, VEN, ZAF, ZMB, ZWE' Missing required fields: summary: Union of required fields across selected services, with the universal-field hint value: error: - 'Missing required fields: tax_number (or use ''identification_number'' which maps to ''tax_number'' for BRA)' fields_required_by: tax_number: - bra_cpf Unknown service id: summary: '`services` contains an id that is not in the catalog' value: services: - 'Unknown service_id(s): [''bogus_service'']' Service not live for country: summary: '`services` names a service from another country (or a stub entry)' value: services: - 'Services not currently live for PER: [''bra_cpf'']. Available services: [''per_dni'', ''per_residential'', ''per_tax_registration'']' Service requires onboarding: summary: Service exists but is not activated for your organization yet value: services: - 'Services require onboarding before they can be enabled for AUS: [''aus_australia_driver_licence'']. Contact Didit support to activate them.' requires_onboarding: - aus_australia_driver_licence Consent missing: summary: A selected service is flagged requires_consent=true and `consent` was not `true` value: consent: - 'Explicit end-user consent is required for these database validation services: [''chn_national_id'']. Send `consent=true`.' services_requiring_consent: - chn_national_id Country format rule violation: summary: Identifier fails the country's format rule (length/digits/regex) value: tax_number: - 'Invalid tax number for BRA: Brazilian CPF (exactly 11 digits). Must be exactly 11 characters long (got 10).' Service-specific format violation: summary: A field fails the selected service's own format requirements value: error: - One or more fields do not match the format required by the selected service. invalid_fields_by_service: chn_national_id: - national_id_format '403': description: 'Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — and also when the calling organization''s balance cannot cover the call. Authentication failures return `403` with `{"detail": ...}`; this API never returns `401`. Credit shortfalls return `403` with `{"error": ...}` before any screening or provider call happens.' content: application/json: examples: Missing or invalid API key: summary: No `x-api-key` header, or the key is invalid/revoked value: detail: You do not have permission to perform this action. Not enough credits: summary: Organization balance cannot cover the call value: error: You don't have enough credits to perform this request. Please top up at https://business.didit.me '429': description: Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers. content: application/json: examples: Write rate limit exceeded: summary: More than 300 write requests in the current minute value: detail: Write request rate limit exceeded. You can make up to 300 requests per minute. schema: type: object properties: detail: type: string '502': description: No selected service returned a usable result (provider outage, empty registry response). Nothing is billed. When `save_api_request=true` (default) the failed attempt is still recorded as a session with status `Not Finished` and a `COULD_NOT_PERFORM_DATABASE_VALIDATION` warning. Safe to retry later. Partial failures do **not** use this code — they return `200` with `database_validation.errors`. content: application/json: schema: type: object properties: error: type: string validation_errors: type: array items: type: object properties: service_id: type: string code: type: string message: type: string examples: Provider returned no result: summary: Every selected service failed value: error: Database validation could not be performed. The external validation service did not return a result. Please try again later. validation_errors: - service_id: bra_cpf code: empty_provider_response message: The provider did not return a database validation result. /v3/phone/send/: post: summary: Send Phone Code description: 'Send a one-time passcode (OTP) to a phone number over WhatsApp, SMS, or another supported channel, then verify it with [`POST /v3/phone/check/`](/standalone-apis/phone-check). **How send and check pair up.** Verification state is keyed by your application plus the E.164 `phone_number` — the check call does not take `request_id`. Each new send creates a pending verification that lives for **5 minutes**: call the check with the same number and the code the user received before the window closes. Calling send again for the same number while a verification is pending re-sends a code for that same verification (`status: "Retry"`, same `request_id`). At most one retry is attached this way (two sends total); a further send starts a fresh verification with a new `request_id`. The 5-minute window is measured from the first send and is not extended by retries. **Delivery channels.** `options.preferred_channel` selects the channel (`whatsapp` by default; `sms`, `telegram`, `voice`, `rcs`, `viber`, and `zalo` are also supported). When the preferred channel is unavailable for the destination country or number, delivery automatically falls back to SMS — the send still reports `Success`, and the channel actually used is reported by the check response (`phone.verification_method` and the `lifecycle` events). **Send statuses.** `Success` — OTP sent for a new verification; `Retry` — OTP re-sent for the pending verification; `Blocked` — the anti-fraud layer refused to send (see `reason`, e.g. `repeated_attempts`, `suspicious`, `spam`). A `Blocked` send immediately finalizes the verification as `Declined` with a high-risk warning, so a follow-up check returns `Expired or Not Found`. **Billing.** The price depends on the destination country and the channel that actually delivers the message — see [Phone Verification Pricing](/getting-started/phone-verification-pricing). Only `Blocked` sends are guaranteed free: any other unbilled send attempt is charged at verification finalization or expiration, even if delivery was never confirmed or the delivery provider reported the message undeliverable. Checks are always free. As an anti-abuse measure, phone verification is disabled until your organization''s first top-up (`403`). **Session persistence.** Every new verification is persisted as an API-type session: `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/`, the verification appears in the Business Console, and `status.updated` webhooks fire as it progresses. **Sandbox.** Sandbox API keys skip delivery and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Success` payload with a random `request_id`; no message is sent and nothing is persisted. Use code `123456` on the sandbox check. **Authentication.** Send your application''s API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`) — this API never returns `401`. **Rate limits.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints, plus an anti-abuse cap of 4 send attempts per phone number per hour; exceeding either returns `429`.' operationId: post_v3phone_send tags: - Standalone APIs x-codeSamples: - lang: curl label: curl source: "curl -X POST https://verification.didit.me/v3/phone/send/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"phone_number\": \"+14155552671\",\n \"options\": {\n \"code_size\": 6,\n \"preferred_channel\": \"whatsapp\",\n \"locale\": \"en-US\"\n },\n \"vendor_data\": \"user-1234\"\n }'" - lang: python label: Python source: "import os, requests\n\nresp = requests.post(\n \"https://verification.didit.me/v3/phone/send/\",\n headers={\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Content-Type\": \"application/json\",\n },\n json={\n \"phone_number\": \"+14155552671\",\n \"options\": {\n \"code_size\": 6,\n \"preferred_channel\": \"whatsapp\",\n \"locale\": \"en-US\",\n },\n \"vendor_data\": \"user-1234\",\n },\n timeout=15,\n)\nresp.raise_for_status()\nprint(resp.json()) # {request_id, status, reason, vendor_data, metadata}\n# Then verify with POST /v3/phone/check/ using the same phone_number" - lang: javascript label: JavaScript source: "const res = await fetch('https://verification.didit.me/v3/phone/send/', {\n method: 'POST',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n phone_number: '+14155552671',\n options: { code_size: 6, preferred_channel: 'whatsapp', locale: 'en-US' },\n vendor_data: 'user-1234',\n }),\n});\nif (!res.ok) throw new Error(`Phone send failed: ${res.status}`);\nconst data = await res.json();\nconsole.log(data); // { request_id, status, reason, vendor_data, metadata }\n// Then verify with POST /v3/phone/check/ using the same phone_number" requestBody: required: true content: application/json: schema: type: object properties: phone_number: type: string maxLength: 20 description: Recipient phone number in E.164 format — leading `+` and country code (e.g., `+14155552671`). Numbers are validated and normalized to E.164; unparseable or invalid numbers return `400` with a `phone_number` field error. example: '+14155552671' options: type: object description: OTP delivery options. All fields are optional. properties: code_size: type: integer minimum: 4 maximum: 8 default: 6 description: Number of digits in the OTP. locale: type: string maxLength: 5 pattern: ^[a-z]{2,3}(-[A-Z]{2,3})?$ description: BCP-47 locale used to localize the verification message (e.g., `en-US`, `fr`, `es`). Invalid formats return `400`. example: en-US preferred_channel: type: string enum: - whatsapp - sms - telegram - voice - rcs - viber - zalo default: whatsapp description: Preferred delivery channel. If the channel is unavailable for the destination country or number, the message automatically falls back to SMS — the send still reports `Success`. signals: type: object description: Optional device and network signals about the end user, forwarded to the anti-fraud layer to improve detection of abusive or automated traffic. All fields are optional. properties: ip: type: string description: IP address of the end user's device. IPv4 or IPv6. example: 192.0.2.1 device_id: type: string maxLength: 255 description: 'Unique identifier of the end user''s device. Android: `ANDROID_ID`; iOS: `identifierForVendor`.' example: 8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2 device_platform: type: string enum: - android - ios - ipados - tvos - web description: Platform of the end user's device. device_model: type: string maxLength: 255 description: Model of the end user's device. example: iPhone17,2 os_version: type: string maxLength: 64 description: Operating-system version of the end user's device. example: 18.0.1 app_version: type: string maxLength: 64 description: Version of your application. example: 1.2.34 user_agent: type: string maxLength: 512 description: User agent of the end user's browser or app. example: Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Mobile/15E148 Safari/604.1 vendor_data: type: string description: Optional caller-controlled identifier (your internal user id, an email, a UUID, etc.) persisted on the session and echoed back in the send response, the matching check response, webhooks, and the Business Console. Use it to correlate Didit's `request_id` with your user record. metadata: type: object nullable: true description: Optional free-form JSON object persisted on the session and echoed back in the send response, the matching check response, webhooks, and the Business Console. required: - phone_number examples: WhatsApp (default): summary: 6-digit OTP via WhatsApp, falling back to SMS automatically value: phone_number: '+14155552671' options: preferred_channel: whatsapp locale: en-US vendor_data: user-1234 SMS with custom code length: summary: Force SMS delivery with a 4-digit code value: phone_number: '+34699999999' options: preferred_channel: sms code_size: 4 locale: es With fraud signals: summary: Forward device signals to strengthen anti-fraud screening value: phone_number: '+14155552671' signals: ip: 192.0.2.1 device_id: 8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2 device_platform: ios device_model: iPhone17,2 os_version: 18.0.1 app_version: 1.2.34 vendor_data: user-1234 responses: '200': description: 'Send acknowledged. Inspect `status`: `Success` and `Retry` mean a code is on its way; `Blocked` means the anti-fraud layer refused and the verification is already finalized as `Declined`. `request_id` is the persisted session id (same id on a `Retry`).' content: application/json: schema: $ref: '#/components/schemas/PhoneVerificationSendResponse' examples: Success: summary: OTP sent for a new verification value: request_id: e39cb057-92fc-4b59-b84e-02fec29a0f24 status: Success reason: null vendor_data: user-1234 metadata: null Retry: summary: Second send for the same number inside the 5-minute window — same request_id value: request_id: e39cb057-92fc-4b59-b84e-02fec29a0f24 status: Retry reason: null vendor_data: user-1234 metadata: null Blocked: summary: Anti-fraud refusal — verification finalized as Declined, nothing billed value: request_id: 0d8af1f4-0c43-46d7-b75c-6e4e22c0a7b3 status: Blocked reason: repeated_attempts vendor_data: user-1234 metadata: null '400': description: 'Validation error. Malformed input returns DRF''s field-error envelope (one array of messages per offending field, nested under `options` for option errors). A number that parses but is rejected at send time (unreachable, or an ineligible line type such as a short code) returns a `{"detail": ...}` envelope instead.' content: application/json: examples: Invalid phone number (field error): summary: Number is not valid E.164 value: phone_number: - Invalid phone number provided. Missing phone number: summary: '`phone_number` not included in the body' value: phone_number: - This field is required. Invalid options: summary: Out-of-range `code_size`, malformed `locale`, unknown `preferred_channel` value: options: code_size: - Ensure this value is less than or equal to 8. locale: - Ensure this field has no more than 5 characters. preferred_channel: - '"carrier_pigeon" is not a valid choice.' Number rejected at send time: summary: Valid E.164 shape but the number cannot receive an OTP value: detail: Invalid phone number provided. Ineligible line type: summary: Line type cannot receive an OTP (e.g., short code) value: detail: Invalid phone line type provided. '403': description: 'Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment (`{"detail": ...}`) — this API never returns `401`. Also returned before any send when the organization has never topped up (anti-abuse gate) or when its balance cannot cover the destination''s send price (`{"error": ...}`).' content: application/json: examples: Missing or invalid API key: summary: No `x-api-key` header, or the key is invalid/revoked value: detail: You do not have permission to perform this action. Top-up required: summary: Phone verification stays disabled until the organization's first top-up value: detail: To protect against abuse, phone verification is disabled until your first top-up. Not enough credits: summary: Organization balance cannot cover the send price value: error: You don't have enough credits to perform this request. '429': description: 'Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. Additionally, an anti-abuse cap of 4 verification attempts per phone number per hour applies upstream. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers. Note: the `X-RateLimit-*`/`Retry-After` headers accompany only the 300/min write-limit 429; the per-number cap 429 carries no rate-limit headers.' content: application/json: examples: Per-number cap: summary: More than 4 verification attempts for the same number in the last hour value: detail: Maximum verification attempts reached for this phone number. Only 4 authentication attempts are allowed per hour. Try again later or use a different number. Write rate limit exceeded: summary: More than 300 write requests in the current minute value: detail: Write request rate limit exceeded. You can make up to 300 requests per minute. schema: type: object properties: detail: type: string '500': description: Unexpected delivery failure while creating the verification (e.g., the delivery provider is unreachable). Safe to retry. content: application/json: examples: Provider error: summary: OTP delivery could not be initiated value: detail: Error creating phone verification schema: type: object properties: detail: type: string security: - ApiKeyAuth: [] /v3/phone/check/: post: summary: Check Phone Code description: 'Verify the OTP delivered by [`POST /v3/phone/send/`](/standalone-apis/phone-send) and get the final verification result plus phone intelligence: carrier name and line type, virtual (VoIP) and disposable flags, and duplicate usage of the number across your sessions. **How the check finds the verification.** Matching is by your application plus the E.164 `phone_number` — `request_id` is not an input. The most recent pending verification created within the last **5 minutes** is checked. If there is none (never sent, already finalized, or older than 5 minutes) the endpoint returns `200` with `status: "Expired or Not Found"`. **Attempt budget.** Each verification allows **3 code attempts**. The first two wrong codes return `status: "Failed"` with the attempts remaining and `phone: null`; the third wrong code finalizes the verification as `Declined` with a `VERIFICATION_CODE_ATTEMPTS_EXCEEDED` warning and returns the full phone report. **Outcomes.** `Approved` — correct code and no declining risk. `Declined` — terminal: the code was correct but a declining risk matched (a `DECLINE` action below, a blocklisted or high-risk number), or the attempt budget was exhausted. `Failed` — wrong code, attempts remaining. `Expired or Not Found` — nothing to check. On `Approved`/`Declined` the `request_id` equals the send''s `request_id` (the session id) and `phone` carries the full report (`carrier`, `is_virtual`, `is_disposable`, `warnings`, `lifecycle`, `matches`); on `Failed` and `Expired or Not Found` the `request_id` is a one-off random UUID. **Risk actions.** `duplicated_phone_number_action`, `disposable_number_action`, and `voip_number_action` decide what happens when the corresponding risk is detected on a correct code: `DECLINE` flips the final status to `Declined`; `NO_ACTION` (default) records the risk in `phone.warnings` without affecting the status. **Billing.** Checks are free — delivery is billed on the send side (see [Phone Verification Pricing](/getting-started/phone-verification-pricing)). **Session persistence.** A finalized check updates the session created by the send (visible in the Business Console, queryable via `GET /v3/session/{sessionId}/decision/`) and fires a `status.updated` webhook. **Sandbox.** Sandbox API keys skip all processing: code `123456` returns a static `Approved` payload with a simplified flat `phone` object (`status`, `phone_number`, `country_code`, `carrier_name`, `line_type`, `flags`); any other code returns `Failed`. Nothing is persisted. **Authentication.** Send your application''s API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`) — this API never returns `401`. **Rate limits.** Shared write budget of 300 requests/min per API key, plus the upstream anti-abuse cap of 4 attempts per number per hour; exceeding either returns `429`.' operationId: post_v3phone_check tags: - Standalone APIs x-codeSamples: - lang: curl label: curl source: "curl -X POST https://verification.didit.me/v3/phone/check/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"phone_number\": \"+14155552671\",\n \"code\": \"123456\",\n \"voip_number_action\": \"DECLINE\"\n }'" - lang: python label: Python source: "import os, requests\n\nresp = requests.post(\n \"https://verification.didit.me/v3/phone/check/\",\n headers={\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Content-Type\": \"application/json\",\n },\n json={\n \"phone_number\": \"+14155552671\", # same number as the send call\n \"code\": \"123456\",\n \"voip_number_action\": \"DECLINE\",\n },\n timeout=15,\n)\nresp.raise_for_status()\nresult = resp.json()\nprint(result[\"status\"]) # Approved / Declined / Failed / Expired or Not Found\nif result[\"status\"] in (\"Approved\", \"Declined\"):\n print(result[\"phone\"][\"carrier\"], result[\"phone\"][\"is_virtual\"])" - lang: javascript label: JavaScript source: "const res = await fetch('https://verification.didit.me/v3/phone/check/', {\n method: 'POST',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n phone_number: '+14155552671', // same number as the send call\n code: '123456',\n voip_number_action: 'DECLINE',\n }),\n});\nconst data = await res.json();\nif (data.status === 'Approved') {\n // full phone report is in data.phone (carrier, is_virtual, matches, ...)\n}" requestBody: required: true content: application/json: schema: type: object properties: phone_number: type: string maxLength: 20 description: The same phone number used in the matching [`POST /v3/phone/send/`](/standalone-apis/phone-send) call, in E.164 format. This is what links the check to the send. example: '+14155552671' code: type: string minLength: 4 maxLength: 8 pattern: ^[0-9]{4,8}$ description: 'The OTP the end user received. Must be 4–8 numeric digits — anything else returns `400` without consuming an attempt. A well-formed but wrong code returns `200` with `status: "Failed"` and consumes one of the 3 attempts.' example: '123456' duplicated_phone_number_action: type: string enum: - NO_ACTION - DECLINE default: NO_ACTION description: What to do when the same number was already used by a different user (different `vendor_data`) of your application. `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `phone.warnings` and fills `phone.matches`. disposable_number_action: type: string enum: - NO_ACTION - DECLINE default: NO_ACTION description: What to do when the carrier lookup flags the number as a temporary/burner line (`phone.is_disposable`). `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `phone.warnings`. voip_number_action: type: string enum: - NO_ACTION - DECLINE default: NO_ACTION description: What to do when the carrier lookup reports a virtual line type — `voip`, `isp`, or `vpn` (`phone.is_virtual`). `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `phone.warnings`. required: - phone_number - code examples: Default: summary: Record any risks but never auto-decline value: phone_number: '+14155552671' code: '123456' Strict risk policy: summary: Auto-decline VoIP and disposable lines value: phone_number: '+14155552671' code: '123456' voip_number_action: DECLINE disposable_number_action: DECLINE responses: '200': description: Check completed — wrong codes and missing verifications also return `200`; inspect `status`, not the HTTP code. `phone` is populated only on finalized outcomes (`Approved`/`Declined`), `null` on `Failed`, and absent on `Expired or Not Found`. content: application/json: schema: $ref: '#/components/schemas/PhoneVerificationCheckResponse' examples: Approved: summary: Correct code — full phone report with carrier, lifecycle, and matches value: request_id: e39cb057-92fc-4b59-b84e-02fec29a0f24 status: Approved message: The verification code is correct. phone: status: Approved phone_number_prefix: '+1' phone_number: '4155552671' full_number: '+14155552671' country_code: US country_name: United States carrier: name: AT&T type: mobile is_disposable: false is_virtual: false verification_method: whatsapp verification_attempts: 1 verified_at: '2026-06-12T01:24:47.311323Z' warnings: [] lifecycle: - type: PHONE_VERIFICATION_MESSAGE_SENT timestamp: '2026-06-12T01:23:39.580554+00:00' details: status: Success reason: null channel: whatsapp actual_channel: whatsapp fee: 0.0709 - type: PHONE_DELIVERY_DELIVERED timestamp: '2026-06-12T01:23:43.118220+00:00' details: channel: whatsapp status: delivered fee: 0 - type: VALID_CODE_ENTERED timestamp: '2026-06-12T01:24:47.311201+00:00' details: code_tried: '123456' status: Approved fee: 0 - type: PHONE_VERIFICATION_APPROVED timestamp: '2026-06-12T01:24:47.384292+00:00' details: null fee: 0 matches: [] vendor_data: user-1234 metadata: null created_at: '2026-06-12T01:24:47.401719+00:00' Failed (attempts remaining): summary: Wrong code — the user can retry; request_id here is a one-off UUID value: request_id: 11111111-2222-3333-4444-555555555555 status: Failed message: 'The verification code is incorrect. Attempts remaining: 2' phone: null vendor_data: user-1234 metadata: null created_at: '2026-06-12T01:24:21.512340+00:00' Declined (max attempts): summary: Third wrong code — verification finalized as Declined value: request_id: e39cb057-92fc-4b59-b84e-02fec29a0f24 status: Declined message: The verification code is incorrect. Maximum attempts reached. phone: status: Declined phone_number_prefix: '+1' phone_number: '4155552671' full_number: '+14155552671' country_code: US country_name: United States carrier: name: AT&T type: mobile is_disposable: false is_virtual: false verification_method: whatsapp verification_attempts: 1 verified_at: null warnings: - feature: PHONE risk: VERIFICATION_CODE_ATTEMPTS_EXCEEDED additional_data: null log_type: error short_description: Verification code attempts exceeded long_description: The system detected that the phone number has exceeded the maximum number of verification code attempts. lifecycle: - type: PHONE_VERIFICATION_MESSAGE_SENT timestamp: '2026-06-12T01:23:39.580554+00:00' details: status: Success reason: null channel: whatsapp actual_channel: whatsapp fee: 0.0709 - type: INVALID_CODE_ENTERED timestamp: '2026-06-12T01:24:01.118220+00:00' details: code_tried: '111111' status: Failed fee: 0 - type: INVALID_CODE_ENTERED timestamp: '2026-06-12T01:24:21.512340+00:00' details: code_tried: '222222' status: Failed fee: 0 - type: INVALID_CODE_ENTERED timestamp: '2026-06-12T01:24:47.311201+00:00' details: code_tried: '333333' status: Failed fee: 0 - type: PHONE_VERIFICATION_DECLINED timestamp: '2026-06-12T01:24:47.384292+00:00' details: reason: VERIFICATION_CODE_ATTEMPTS_EXCEEDED fee: 0 matches: [] vendor_data: user-1234 metadata: null created_at: '2026-06-12T01:24:47.401719+00:00' Declined (risk policy): summary: Correct code, but the line is VoIP and voip_number_action=DECLINE value: request_id: 7f1c9d2e-44a1-4b6e-9a3e-5b8e6f1c2d3a status: Declined message: The verification code is correct. phone: status: Declined phone_number_prefix: '+1' phone_number: '5005550006' full_number: '+15005550006' country_code: US country_name: United States carrier: name: CLEC LLC type: voip is_disposable: false is_virtual: true verification_method: sms verification_attempts: 1 verified_at: '2026-06-12T01:24:47.311323Z' warnings: - feature: PHONE risk: VOIP_NUMBER_DETECTED additional_data: null log_type: error short_description: VoIP number detected long_description: The system detected that the phone number is a VoIP number, which is not allowed. lifecycle: - type: PHONE_VERIFICATION_MESSAGE_SENT timestamp: '2026-06-12T01:23:39.580554+00:00' details: status: Success reason: null channel: whatsapp actual_channel: sms fee: 0.0709 - type: VALID_CODE_ENTERED timestamp: '2026-06-12T01:24:47.311201+00:00' details: code_tried: '123456' status: Approved fee: 0 - type: PHONE_VERIFICATION_DECLINED timestamp: '2026-06-12T01:24:47.384292+00:00' details: reason: VOIP_NUMBER_DETECTED fee: 0 matches: [] vendor_data: user-1234 metadata: null created_at: '2026-06-12T01:24:47.401719+00:00' Expired or Not Found: summary: No pending verification for this number in the last 5 minutes value: request_id: 69cc35c6-280a-47a6-ab8c-4f32abc02ed0 status: Expired or Not Found message: No pending phone verification found in the last 5 minutes. vendor_data: null metadata: null created_at: '2026-06-12T01:24:47.311323+00:00' '400': description: 'Validation error — DRF''s field-error envelope: one array of messages per offending field.' content: application/json: examples: Invalid code format: summary: '`code` is not 4–8 numeric digits' value: code: - Invalid code format. Enter 4 to 8 numeric digits. Invalid phone number: summary: '`phone_number` is not valid E.164' value: phone_number: - Invalid phone number provided. Invalid action: summary: Risk actions accept only NO_ACTION or DECLINE value: voip_number_action: - '"REVIEW" is not a valid choice.' '403': description: Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — this API never returns `401`. Checks are free, so there is no credit check here. content: application/json: examples: Missing or invalid API key: summary: No `x-api-key` header, or the key is invalid/revoked value: detail: You do not have permission to perform this action. '429': description: 'Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. Additionally, an anti-abuse cap of 4 verification attempts per phone number per hour applies upstream. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers. Note: the `X-RateLimit-*`/`Retry-After` headers accompany only the 300/min write-limit 429; the per-number cap 429 carries no rate-limit headers.' content: application/json: examples: Per-number cap: summary: More than 4 verification attempts for the same number in the last hour value: detail: Maximum verification attempts reached for this phone number. Only 4 authentication attempts are allowed per hour. Try again later or use a different number. Write rate limit exceeded: summary: More than 300 write requests in the current minute value: detail: Write request rate limit exceeded. You can make up to 300 requests per minute. schema: type: object properties: detail: type: string '500': description: Unexpected failure while verifying the code upstream. Safe to retry; the attempt budget is only consumed by completed checks. content: application/json: examples: Provider error: summary: Code verification could not be completed value: detail: Error checking phone verification schema: type: object properties: detail: type: string security: - ApiKeyAuth: [] /v3/email/send/: post: summary: Send Email Code description: 'Send a one-time passcode (OTP) to an email address, then verify it with [`POST /v3/email/check/`](/standalone-apis/email-check). **How send and check pair up.** Verification state is keyed by your application plus the `email` address — the check call does not take `request_id`. Each new send creates a pending verification that lives for **5 minutes**: call the check with the same address and the code the user received before the window closes. Calling send again for the same address while a verification is pending generates a fresh code for that same verification (`status: "Retry"`, same `request_id`); the previous code stops working. At most one retry is attached this way (two sends total); a further send starts a fresh verification with a new `request_id`. The 5-minute window is measured from the first send and is not extended by retries. **Deliverability pre-check.** Before anything is sent, the address goes through syntax and DNS/MX validation. Addresses that cannot receive mail return `200` with `status: "Undeliverable"` and `reason: "email_can_not_be_delivered"` — no email is sent, nothing is billed, and the verification is immediately finalized as `Declined` (a follow-up check returns `Expired or Not Found`). A downstream send failure reports the same way. **Code format and branding.** Codes are 4–8 characters (`options.code_size`, default 6), numeric by default; set `options.alphanumeric_code: true` for uppercase letters and digits (the check comparison is case-insensitive). The email template is localized via `options.locale` (54 supported languages) and can use your application''s white-label branding via `options.use_white_label_customization`. **Billing.** One Email Verification API credit per successful send (`status: "Success"`), charged at send time. `Retry` and `Undeliverable` sends are free, and checks are free. **Session persistence.** Every new verification is persisted as an API-type session: `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/`, the verification appears in the Business Console, and `status.updated` webhooks fire as it progresses. **Sandbox.** Sandbox API keys skip delivery and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Success` payload with a random `request_id`; no email is sent and nothing is persisted. Use code `123456` on the sandbox check. **Authentication.** Send your application''s API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`) — this API never returns `401`. **Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.' operationId: post_v3email_send tags: - Standalone APIs x-codeSamples: - lang: curl label: curl source: "curl -X POST https://verification.didit.me/v3/email/send/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"email\": \"alice@example.com\",\n \"options\": { \"code_size\": 6, \"locale\": \"en\" },\n \"vendor_data\": \"user-1234\"\n }'" - lang: python label: Python source: "import os, requests\n\nresp = requests.post(\n \"https://verification.didit.me/v3/email/send/\",\n headers={\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Content-Type\": \"application/json\",\n },\n json={\n \"email\": \"alice@example.com\",\n \"options\": {\"code_size\": 6, \"locale\": \"en\"},\n \"vendor_data\": \"user-1234\",\n },\n timeout=15,\n)\nresp.raise_for_status()\nprint(resp.json()) # {request_id, status, reason, vendor_data, metadata}\n# Then verify with POST /v3/email/check/ using the same email" - lang: javascript label: JavaScript source: "const res = await fetch('https://verification.didit.me/v3/email/send/', {\n method: 'POST',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n email: 'alice@example.com',\n options: { code_size: 6, locale: 'en' },\n vendor_data: 'user-1234',\n }),\n});\nif (!res.ok) throw new Error(`Email send failed: ${res.status}`);\nconst data = await res.json();\nconsole.log(data); // { request_id, status, reason, vendor_data, metadata }\n// Then verify with POST /v3/email/check/ using the same email" requestBody: required: true content: application/json: schema: type: object properties: email: type: string format: email description: 'Recipient email address. Malformed addresses return `400`; syntactically valid addresses that cannot receive mail (failed DNS/MX validation) return `200` with `status: "Undeliverable"`.' example: alice@example.com options: type: object description: OTP format, localization, and branding options. All fields are optional. properties: code_size: type: integer minimum: 4 maximum: 8 default: 6 description: Number of characters in the OTP (digits by default; uppercase letters and digits when `alphanumeric_code` is `true`). alphanumeric_code: type: boolean default: false description: When `true`, the OTP is composed of uppercase letters `A–Z` and digits `0–9` instead of digits only. The `/v3/email/check/` comparison is case-insensitive. locale: type: string maxLength: 5 enum: - en - ar - bn - bg - bs - ca - cs - da - de - el - es - et - fa - fi - fr - he - hi - hr - hu - hy - id - it - ja - ka - kk - ko - ky - lt - lv - cnr - mk - mn - ms - nl - 'no' - pl - pt-BR - pt - ro - ru - sk - sl - so - sq - sr - sv - th - tr - uk - uz - vi - zh-CN - zh-TW - zh description: Language of the OTP email template. Defaults to `en`. Unsupported values return `400` listing all supported locales. example: en use_white_label_customization: type: boolean default: false description: When `true`, the OTP email uses your application's white-label customization (logo, colors, sender branding) instead of the default Didit template. signals: type: object description: Optional device and network signals about the end user, forwarded to the anti-fraud layer to improve detection of abusive or automated traffic. All fields are optional. properties: ip: type: string description: IP address of the end user's device. IPv4 or IPv6. example: 192.0.2.1 device_id: type: string maxLength: 255 description: 'Unique identifier of the end user''s device. Android: `ANDROID_ID`; iOS: `identifierForVendor`.' example: 8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2 device_platform: type: string enum: - android - ios - ipados - tvos - web description: Platform of the end user's device. device_model: type: string maxLength: 255 description: Model of the end user's device. example: iPhone17,2 os_version: type: string maxLength: 64 description: Operating-system version of the end user's device. example: 18.0.1 app_version: type: string maxLength: 64 description: Version of your application. example: 1.2.34 user_agent: type: string maxLength: 512 description: User agent of the end user's browser or app. example: Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Mobile/15E148 Safari/604.1 vendor_data: type: string description: Optional caller-controlled identifier (your internal user id, an email, a UUID, etc.) persisted on the session and echoed back in the send response, the matching check response, webhooks, and the Business Console. Use it to correlate Didit's `request_id` with your user record. metadata: type: object nullable: true description: Optional free-form JSON object persisted on the session and echoed back in the send response, the matching check response, webhooks, and the Business Console. required: - email examples: Numeric 6-digit code: summary: Default — 6-digit numeric code in English value: email: alice@example.com options: locale: en vendor_data: user-1234 Alphanumeric 8-character code: summary: Uppercase letters + digits, Spanish template value: email: bob@example.com options: code_size: 8 alphanumeric_code: true locale: es White-label branding: summary: Send the OTP email with your application's white-label customization value: email: carol@example.com options: locale: en use_white_label_customization: true vendor_data: user-5678 responses: '200': description: 'Send acknowledged. Inspect `status`: `Success` and `Retry` mean a code is on its way; `Undeliverable` means the address cannot receive mail and the verification is already finalized as `Declined`. `request_id` is the persisted session id (same id on a `Retry`).' content: application/json: schema: $ref: '#/components/schemas/EmailVerificationSendResponse' examples: Success: summary: OTP emailed for a new verification (billed) value: request_id: e39cb057-92fc-4b59-b84e-02fec29a0f24 status: Success reason: null vendor_data: user-1234 metadata: null Retry: summary: Second send inside the 5-minute window — fresh code, same request_id, free value: request_id: e39cb057-92fc-4b59-b84e-02fec29a0f24 status: Retry reason: null vendor_data: user-1234 metadata: null Undeliverable: summary: Address failed DNS/MX validation — verification finalized as Declined, nothing billed value: request_id: cb1a5011-344d-444d-bdce-218904745d7f status: Undeliverable reason: email_can_not_be_delivered vendor_data: user-1234 metadata: null '400': description: 'Validation error — DRF''s field-error envelope: one array of messages per offending field, nested under `options` for option errors.' content: application/json: examples: Invalid email: summary: Malformed email address value: email: - Enter a valid email address. Unsupported locale: summary: '`options.locale` outside the supported set' value: options: locale: - Invalid locale. Supported locales are en, ar, bn, bg, bs, ca, cs, da, de, el, es, et, fa, fi, fr, he, hi, hr, hu, hy, id, it, ja, ka, kk, ko, ky, lt, lv, cnr, mk, mn, ms, nl, no, pl, pt-BR, pt, ro, ru, sk, sl, so, sq, sr, sv, th, tr, uk, uz, vi, zh-CN, zh-TW, zh. Code size out of range: summary: '`options.code_size` outside 4–8' value: options: code_size: - Ensure this value is less than or equal to 8. '403': description: 'Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment (`{"detail": ...}`) — this API never returns `401`. Also returned before any send when the organization''s balance cannot cover one Email Verification API credit (`{"error": ...}`).' content: application/json: examples: Missing or invalid API key: summary: No `x-api-key` header, or the key is invalid/revoked value: detail: You do not have permission to perform this action. Not enough credits: summary: Organization balance cannot cover the send price value: error: You don't have enough credits to perform this request. '429': description: Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers. content: application/json: examples: Write rate limit exceeded: summary: More than 300 write requests in the current minute value: detail: Write request rate limit exceeded. You can make up to 300 requests per minute. schema: type: object properties: detail: type: string security: - ApiKeyAuth: [] /v3/email/check/: post: summary: Check Email Code description: 'Verify the OTP delivered by [`POST /v3/email/send/`](/standalone-apis/email-send) and get the final verification result plus email intelligence: deliverability, breach exposure (`is_breached` and the `breaches` list from a breach-intelligence database), disposable-provider detection, and duplicate usage of the address across your sessions. **How the check finds the verification.** Matching is by your application plus the `email` address — `request_id` is not an input. The most recent pending verification created within the last **5 minutes** is checked. If there is none (never sent, already finalized, undeliverable at send time, or older than 5 minutes) the endpoint returns `200` with `status: "Expired or Not Found"`. **Attempt budget.** Each verification allows **3 code attempts**. The first two wrong codes return `status: "Failed"` with the attempts remaining and `email: null`; the third wrong code finalizes the verification as `Declined` with an `EMAIL_CODE_ATTEMPTS_EXCEEDED` warning and returns the full email report. Codes are compared case-insensitively (relevant for `alphanumeric_code` sends); only the most recently sent code is valid. **Outcomes.** `Approved` — correct code and no declining risk. `Declined` — terminal: the code was correct but a declining risk matched (a `DECLINE` action below, a blocklisted address, or the address found undeliverable at finalization), or the attempt budget was exhausted. `Failed` — wrong code, attempts remaining. `Expired or Not Found` — nothing to check. On `Approved`/`Declined` the `request_id` equals the send''s `request_id` (the session id) and `email` carries the full report (`is_breached`, `breaches`, `is_disposable`, `is_undeliverable`, `warnings`, `lifecycle`, `matches`); on `Failed` and `Expired or Not Found` the `request_id` is a one-off random UUID. **Risk actions.** `duplicated_email_action`, `breached_email_action`, and `disposable_email_action` decide what happens when the corresponding risk is detected on a correct code: `DECLINE` flips the final status to `Declined`; `NO_ACTION` (default) records the risk in `email.warnings` without affecting the status. **Billing.** Checks are free — the credit is consumed by the successful send. **Session persistence.** A finalized check updates the session created by the send (visible in the Business Console, queryable via `GET /v3/session/{sessionId}/decision/`) and fires a `status.updated` webhook. **Sandbox.** Sandbox API keys skip all processing: code `123456` returns a static `Approved` payload with a simplified `email` object (`status`, `email`, `is_breached`, `is_disposable`, `is_undeliverable`); any other code returns `Failed`. Nothing is persisted. **Authentication.** Send your application''s API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`) — this API never returns `401`. **Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.' operationId: post_v3email_check tags: - Standalone APIs x-codeSamples: - lang: curl label: curl source: "curl -X POST https://verification.didit.me/v3/email/check/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"email\": \"alice@example.com\",\n \"code\": \"123456\",\n \"disposable_email_action\": \"DECLINE\"\n }'" - lang: python label: Python source: "import os, requests\n\nresp = requests.post(\n \"https://verification.didit.me/v3/email/check/\",\n headers={\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Content-Type\": \"application/json\",\n },\n json={\n \"email\": \"alice@example.com\", # same address as the send call\n \"code\": \"123456\",\n \"disposable_email_action\": \"DECLINE\",\n },\n timeout=15,\n)\nresp.raise_for_status()\nresult = resp.json()\nprint(result[\"status\"]) # Approved / Declined / Failed / Expired or Not Found\nif result[\"status\"] in (\"Approved\", \"Declined\"):\n print(result[\"email\"][\"is_breached\"], result[\"email\"][\"is_disposable\"])" - lang: javascript label: JavaScript source: "const res = await fetch('https://verification.didit.me/v3/email/check/', {\n method: 'POST',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n email: 'alice@example.com', // same address as the send call\n code: '123456',\n disposable_email_action: 'DECLINE',\n }),\n});\nconst data = await res.json();\nif (data.status === 'Approved') {\n // full email report is in data.email (breaches, is_disposable, matches, ...)\n}" requestBody: required: true content: application/json: schema: type: object properties: email: type: string format: email description: The same email address used in the matching [`POST /v3/email/send/`](/standalone-apis/email-send) call. This is what links the check to the send. example: alice@example.com code: type: string maxLength: 10 description: 'The OTP the end user received: 4–8 digits, or 4–8 letters/digits when `alphanumeric_code: true` was used at send time. Comparison is case-insensitive.' example: '123456' duplicated_email_action: type: string enum: - NO_ACTION - DECLINE default: NO_ACTION description: What to do when the same address was already used and approved by a different user (only previously Approved verifications count) (different `vendor_data`) of your application. `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `email.warnings` and fills `email.matches`. breached_email_action: type: string enum: - NO_ACTION - DECLINE default: NO_ACTION description: What to do when the address appears in known data breaches (`email.is_breached`). `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `email.warnings`. disposable_email_action: type: string enum: - NO_ACTION - DECLINE default: NO_ACTION description: What to do when the domain belongs to a disposable/temporary-mail provider (`email.is_disposable`). `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `email.warnings`. required: - email - code examples: Default: summary: Record any risks but never auto-decline value: email: alice@example.com code: '123456' Strict risk policy: summary: Auto-decline disposable and breached inboxes value: email: alice@example.com code: '123456' disposable_email_action: DECLINE breached_email_action: DECLINE responses: '200': description: Check completed — wrong codes and missing verifications also return `200`; inspect `status`, not the HTTP code. `email` is populated only on finalized outcomes (`Approved`/`Declined`), `null` on `Failed`, and absent on `Expired or Not Found`. content: application/json: schema: $ref: '#/components/schemas/EmailVerificationCheckResponse' examples: Approved: summary: Correct code — clean address, full email report value: request_id: e39cb057-92fc-4b59-b84e-02fec29a0f24 status: Approved message: The verification code is correct. email: status: Approved email: alice@example.com is_breached: false breaches: [] is_disposable: false is_undeliverable: false verification_attempts: 1 verified_at: '2026-06-12T01:24:47.311323Z' warnings: [] lifecycle: - type: EMAIL_VERIFICATION_MESSAGE_SENT timestamp: '2026-06-12T01:23:39.580554+00:00' details: status: Success reason: null fee: 0.03 - type: VALID_CODE_ENTERED timestamp: '2026-06-12T01:24:47.311201+00:00' details: code_tried: '123456' status: Approved fee: 0 - type: EMAIL_VERIFICATION_APPROVED timestamp: '2026-06-12T01:24:47.384292+00:00' details: null fee: 0 matches: [] vendor_data: user-1234 metadata: null created_at: '2026-06-12T01:24:47.401719+00:00' Approved with breach exposure: summary: Correct code; breach exposure recorded as a warning (breached_email_action=NO_ACTION) value: request_id: 3a1f2b6c-9d4e-4f7a-8b2c-1e5d6f7a8b9c status: Approved message: The verification code is correct. email: status: Approved email: bob@example.com is_breached: true breaches: - name: ExampleApp domain: exampleapp.com breach_date: '2019-03-21' breach_emails_count: 763117 description: In March 2019, the social platform ExampleApp suffered a data breach that exposed email addresses and passwords. logo_path: https:///logos/ExampleApp.png data_classes: - email_addresses - passwords is_verified: true is_disposable: false is_undeliverable: false verification_attempts: 1 verified_at: '2026-06-12T01:24:47.311323Z' warnings: - feature: EMAIL risk: BREACHED_EMAIL_DETECTED additional_data: null log_type: information short_description: Breached email detected long_description: This email address was found in one or more known data breaches. lifecycle: - type: EMAIL_VERIFICATION_MESSAGE_SENT timestamp: '2026-06-12T01:23:39.580554+00:00' details: status: Success reason: null fee: 0.03 - type: VALID_CODE_ENTERED timestamp: '2026-06-12T01:24:47.311201+00:00' details: code_tried: '835126' status: Approved fee: 0 - type: EMAIL_VERIFICATION_APPROVED timestamp: '2026-06-12T01:24:47.384292+00:00' details: null fee: 0 matches: [] vendor_data: user-5678 metadata: null created_at: '2026-06-12T01:24:47.401719+00:00' Failed (attempts remaining): summary: Wrong code — the user can retry; request_id here is a one-off UUID value: request_id: 11111111-2222-3333-4444-555555555555 status: Failed message: 'The verification code is incorrect. Attempts remaining: 2' email: null vendor_data: user-1234 metadata: null created_at: '2026-06-12T01:24:21.512340+00:00' Declined (risk policy): summary: Correct code, but the address is disposable and disposable_email_action=DECLINE value: request_id: 8c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f status: Declined message: The verification code is correct. email: status: Declined email: tempuser42@mailinator.com is_breached: false breaches: [] is_disposable: true is_undeliverable: false verification_attempts: 1 verified_at: '2026-06-12T01:24:47.311323Z' warnings: - feature: EMAIL risk: DISPOSABLE_EMAIL_DETECTED additional_data: null log_type: error short_description: Disposable email detected long_description: The system detected that the email is disposable, which is not allowed. lifecycle: - type: EMAIL_VERIFICATION_MESSAGE_SENT timestamp: '2026-06-12T01:23:39.580554+00:00' details: status: Success reason: null fee: 0.03 - type: VALID_CODE_ENTERED timestamp: '2026-06-12T01:24:47.311201+00:00' details: code_tried: '123456' status: Approved fee: 0 - type: EMAIL_VERIFICATION_DECLINED timestamp: '2026-06-12T01:24:47.384292+00:00' details: reason: DISPOSABLE_EMAIL_DETECTED fee: 0 matches: [] vendor_data: user-1234 metadata: null created_at: '2026-06-12T01:24:47.401719+00:00' Expired or Not Found: summary: No pending verification for this address in the last 5 minutes value: request_id: 88808a77-06e9-4cb0-85f5-9c052ddfd987 status: Expired or Not Found message: No pending email verification found in the last 5 minutes. vendor_data: null metadata: null created_at: '2026-06-12T01:24:59.887904+00:00' '400': description: 'Validation error — DRF''s field-error envelope: one array of messages per offending field.' content: application/json: examples: Invalid email: summary: Malformed email address value: email: - Enter a valid email address. Missing code: summary: '`code` not included in the body' value: code: - This field is required. '403': description: Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — this API never returns `401`. Checks are free, so there is no credit check here. content: application/json: examples: Missing or invalid API key: summary: No `x-api-key` header, or the key is invalid/revoked value: detail: You do not have permission to perform this action. '429': description: Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers. content: application/json: examples: Write rate limit exceeded: summary: More than 300 write requests in the current minute value: detail: Write request rate limit exceeded. You can make up to 300 requests per minute. schema: type: object properties: detail: type: string security: - ApiKeyAuth: [] components: schemas: PhoneVerificationSendResponse: type: object properties: request_id: type: string format: uuid description: Session id of the verification. A `Retry` send returns the same `request_id` as the original send. This id appears in the Business Console, is returned again by a finalized `POST /v3/phone/check/`, and can be passed to `GET /v3/session/{sessionId}/decision/`. status: type: string enum: - Success - Retry - Blocked description: '`Success` — OTP sent to a new verification. `Retry` — OTP re-sent for the pending verification created by a previous send. `Blocked` — the anti-fraud layer refused to send; the verification is immediately finalized as `Declined` and nothing is billed.' reason: type: string nullable: true description: Why a send was `Blocked` (e.g., `repeated_attempts`, `suspicious`, `spam`). `null` on `Success` and `Retry`. vendor_data: type: string nullable: true description: Echo of the `vendor_data` stored on the session (from the first send). metadata: type: object nullable: true description: Echo of the `metadata` stored on the session (from the first send). EmailVerificationSendResponse: type: object properties: request_id: type: string format: uuid description: Session id of the verification. A `Retry` send returns the same `request_id` as the original send. This id appears in the Business Console, is returned again by a finalized `POST /v3/email/check/`, and can be passed to `GET /v3/session/{sessionId}/decision/`. status: type: string enum: - Success - Retry - Undeliverable description: '`Success` — OTP emailed to a new verification (billed). `Retry` — fresh OTP emailed for the pending verification created by a previous send (free). `Undeliverable` — the address failed deliverability validation or the message could not be sent; the verification is immediately finalized as `Declined` and nothing is billed.' reason: type: string nullable: true enum: - email_can_not_be_delivered - null description: '`email_can_not_be_delivered` when `status` is `Undeliverable`; `null` otherwise.' vendor_data: type: string nullable: true description: Echo of the `vendor_data` stored on the session (from the first send). metadata: type: object nullable: true description: Echo of the `metadata` stored on the session (from the first send). EmailVerificationCheckResponse: type: object properties: request_id: type: string format: uuid description: 'On `Approved`/`Declined`: the session id of the matched verification — identical to the `request_id` returned by `POST /v3/email/send/`. On `Failed` and `Expired or Not Found`: a random one-off UUID that cannot be looked up later.' status: type: string enum: - Approved - Declined - Failed - Expired or Not Found description: '`Approved` — correct code, no declining risk. `Declined` — terminal: a declining risk matched or the attempt budget (3) was exhausted. `Failed` — wrong code, attempts remaining. `Expired or Not Found` — no pending verification for this address in the last 5 minutes.' message: type: string description: Human-readable explanation of the outcome, including the number of attempts remaining after a wrong code. email: type: object nullable: true description: Full email report. Present (non-null) only on finalized outcomes (`Approved`/`Declined`); `null` on `Failed` and absent on `Expired or Not Found`. properties: status: type: string enum: - Approved - Declined - In Review - Not Finished - Expired description: Final status of the email verification. Matches the top-level `status` for finalized checks. email: type: string format: email description: The verified email address. example: alice@example.com is_breached: type: boolean description: '`true` when the address appears in known data breaches (breach intelligence lookup run at finalization).' breaches: type: array description: Up to 5 most recent known breaches containing this address. Empty array (or omitted) when `is_breached` is `false`. items: type: object properties: name: type: string description: Breach identifier. domain: type: string description: Domain of the breached service. breach_date: type: string format: date description: Date the breach occurred. breach_emails_count: type: integer description: Total number of accounts exposed in the breach. description: type: string description: Human-readable description of the breach (may contain HTML markup). logo_path: type: string description: URL of the breached service's logo. data_classes: type: array items: type: string description: Categories of data exposed, in snake_case (e.g., `email_addresses`, `passwords`). is_verified: type: boolean description: Whether the breach has been verified as legitimate. is_disposable: type: boolean description: '`true` when the domain belongs to a known disposable/temporary-mail provider.' is_undeliverable: type: boolean description: '`true` when the address cannot receive mail (failed syntax or DNS/MX validation, or the message bounced).' verification_attempts: type: integer description: Number of OTP sends for this verification (1, or 2 after a retry). verified_at: type: string format: date-time nullable: true description: When the correct code was entered. `null` if the verification was never approved. warnings: type: array description: Risk warnings recorded during the verification (duplicate, breached, disposable, undeliverable, attempts exceeded, blocklist, etc.). Empty array when there are none. items: type: object properties: feature: type: string description: Feature that produced the warning. Always `EMAIL` here. risk: type: string description: Machine-readable risk code. additional_data: type: object nullable: true description: Risk-specific context (e.g., the duplicated session id for duplicate risks). `null` when the risk carries no extra data. log_type: type: string enum: - error - warning - information description: Severity. `error` risks decline the verification, `warning` flags it for review, `information` is recorded without affecting the status. short_description: type: string description: Human-readable one-line summary of the risk. long_description: type: string description: Human-readable explanation of the risk. lifecycle: type: array description: Chronological audit trail of the verification. Event `type` is one of `EMAIL_VERIFICATION_MESSAGE_SENT`, `EMAIL_VERIFICATION_RETRY_MESSAGE_SENT`, `VALID_CODE_ENTERED`, `INVALID_CODE_ENTERED`, `EMAIL_VERIFICATION_APPROVED`, `EMAIL_VERIFICATION_DECLINED`, `EMAIL_VERIFICATION_IN_REVIEW`, `EMAIL_VERIFICATION_EXPIRED`. items: type: object properties: type: type: string description: Event type. timestamp: type: string format: date-time description: When the event happened. details: type: object nullable: true description: 'Event-specific payload. Send events: `{status, reason}`. Code-entry events: `{code_tried, status}`. Final-status events: `null`, or `{reason}` when declined or in review.' fee: type: number description: USD amount billed for this event. Non-zero only on the first successful send; 0 for retries, code entries, and status events. matches: type: array description: Other sessions of your application (KYC, KYB, or API) where the same email address was used by a different user (different `vendor_data`). Up to 5 entries; a synthetic list-entry blocklist match is prepended when present (and only when no session-derived blocklisted email exists) — session-derived matches keep created_at order. Empty array when there are no matches. items: type: object properties: session_id: type: string format: uuid description: Session id of the other verification that used this email address. session_number: type: integer description: Human-friendly sequential number of that session. vendor_data: type: string nullable: true description: '`vendor_data` of that session.' verification_date: type: string description: When that session was created (`YYYY-MM-DDTHH:MM:SSZ`). email: type: string description: The matched email address. status: type: string description: Status of that session (e.g., `Approved`, `Declined`, `In Progress`). is_blocklisted: type: boolean description: Whether the email address is on your application's blocklist. api_service: type: string nullable: true description: For API-type sessions, the standalone API that created it (e.g., `PHONE_VERIFICATION`); `null` for regular verification sessions. source: type: string description: 'Where the match was found. Usually `session`; a blocklist hit via an organization list entry prepends a synthetic match with `source: "list_entry"` and null `session_id`/`session_number`.' vendor_data: type: string nullable: true description: '`vendor_data` of the matched verification''s session. `null` on `Expired or Not Found`.' metadata: type: object nullable: true description: '`metadata` of the matched verification''s session. `null` on `Expired or Not Found`.' created_at: type: string format: date-time description: Timestamp of this check response. PhoneVerificationCheckResponse: type: object properties: request_id: type: string format: uuid description: 'On `Approved`/`Declined`: the session id of the matched verification — identical to the `request_id` returned by `POST /v3/phone/send/`. On `Failed` and `Expired or Not Found`: a random one-off UUID that cannot be looked up later.' status: type: string enum: - Approved - Declined - Failed - Expired or Not Found description: '`Approved` — correct code, no declining risk. `Declined` — terminal: a declining risk matched or the attempt budget (3) was exhausted. `Failed` — wrong code, attempts remaining. `Expired or Not Found` — no pending verification for this number in the last 5 minutes.' message: type: string description: Human-readable explanation of the outcome, including the number of attempts remaining after a wrong code. phone: type: object nullable: true description: Full phone report. Present (non-null) only on finalized outcomes (`Approved`/`Declined`); `null` on `Failed` and absent on `Expired or Not Found`. properties: status: type: string enum: - Approved - Declined - In Review - Not Finished - Expired description: Final status of the phone verification. Matches the top-level `status` for finalized checks. phone_number_prefix: type: string description: Country calling code with leading `+` (e.g., `+1`). example: '+1' phone_number: type: string description: National number without the country calling code. example: '4155552671' full_number: type: string description: Complete number in E.164 format. example: '+14155552671' country_code: type: string nullable: true description: ISO 3166-1 alpha-2 country of the number. example: US country_name: type: string description: Full country name derived from `country_code`. example: United States carrier: type: object description: Current network serving the number, from the carrier lookup run at finalization. properties: name: type: string description: Carrier name. Empty string when the lookup could not identify it. type: type: string description: 'Line type reported by the lookup: `mobile`, `fixed_line`, `voip`, `isp`, `vpn`, `toll_free`, `premium_rate`, `pager`, `payphone`, `satellite`, `service`, `shared_cost`, `short_codes_commercial`, `calling_cards`, `local_rate`, `universal_access`, `voice_mail`, `other`, or `unknown`.' is_disposable: type: boolean description: '`true` when the number belongs to a temporary/burner number service.' is_virtual: type: boolean description: '`true` when the line type is virtual (`voip`, `isp`, or `vpn`).' verification_method: type: string description: Channel that actually delivered the OTP once delivery is confirmed (e.g., `whatsapp`, `sms`); until then, the requested channel. verification_attempts: type: integer description: Number of OTP sends for this verification (1, or 2 after a retry). verified_at: type: string format: date-time nullable: true description: When the correct code was entered. `null` if the verification was never approved. warnings: type: array description: Risk warnings recorded during the verification (duplicate, disposable, VoIP, attempts exceeded, blocklist, etc.). Empty array when there are none. items: type: object properties: feature: type: string description: Feature that produced the warning. Always `PHONE` here. risk: type: string description: Machine-readable risk code. additional_data: type: object nullable: true description: Risk-specific context (e.g., the duplicated session id for duplicate risks). `null` when the risk carries no extra data. log_type: type: string enum: - error - warning - information description: Severity. `error` risks decline the verification, `warning` flags it for review, `information` is recorded without affecting the status. short_description: type: string description: Human-readable one-line summary of the risk. long_description: type: string description: Human-readable explanation of the risk. lifecycle: type: array description: Chronological audit trail of the verification. Event `type` is one of `PHONE_VERIFICATION_MESSAGE_SENT`, `PHONE_VERIFICATION_RETRY_MESSAGE_SENT`, `PHONE_VERIFICATION_BLOCKED`, `PHONE_DELIVERY_DELIVERED`, `PHONE_DELIVERY_UNDELIVERABLE`, `VALID_CODE_ENTERED`, `INVALID_CODE_ENTERED`, `PHONE_VERIFICATION_APPROVED`, `PHONE_VERIFICATION_DECLINED`, `PHONE_VERIFICATION_IN_REVIEW`, `PHONE_VERIFICATION_EXPIRED`. items: type: object properties: type: type: string description: Event type. timestamp: type: string format: date-time description: When the event happened. details: type: object nullable: true description: 'Event-specific payload. Send events: `{status, reason, channel, actual_channel}` (`actual_channel` is filled once delivery is confirmed). Delivery events: `{channel, status}`. Code-entry events: `{code_tried, status}`. Final-status events: `null`, or `{reason}` when declined or in review.' fee: type: number description: USD amount billed for this event. Non-zero only on send events (the delivery price); 0 for code entries, delivery confirmations, and status events. matches: type: array description: Other sessions of your application (KYC, KYB, or API) where the same phone number was used by a different user (different `vendor_data`). Up to 5 entries, a synthetic list-entry blocklist match first when present (session-derived matches keep query order). Empty array when there are no matches. items: type: object properties: session_id: type: string format: uuid description: Session id of the other verification that used this phone number. session_number: type: integer description: Human-friendly sequential number of that session. vendor_data: type: string nullable: true description: '`vendor_data` of that session.' verification_date: type: string description: When that session was created (`YYYY-MM-DDTHH:MM:SSZ`). phone_number: type: string description: The matched phone number. status: type: string description: Status of that session (e.g., `Approved`, `Declined`, `In Progress`). is_blocklisted: type: boolean description: Whether the phone number is on your application's blocklist. api_service: type: string nullable: true description: For API-type sessions, the standalone API that created it (e.g., `PHONE_VERIFICATION`); `null` for regular verification sessions. source: type: string description: 'Where the match was found. Usually `session`; a blocklist hit via an organization list entry prepends a synthetic match with `source: "list_entry"` and null `session_id`/`session_number`.' vendor_data: type: string nullable: true description: '`vendor_data` of the matched verification''s session. `null` on `Expired or Not Found`.' metadata: type: object nullable: true description: '`metadata` of the matched verification''s session. `null` on `Expired or Not Found`.' created_at: type: string format: date-time description: Timestamp of this check response. securitySchemes: ApiKeyAuth: type: apiKey in: header name: x-api-key TransactionTokenAuth: type: apiKey in: header name: X-Transaction-Token description: Short-lived scoped token minted by your backend via POST /v3/transactions/sdk-token/. Used by the Didit SDKs on the device-facing /v1/transactions/ endpoints. SessionTokenAuth: type: apiKey in: header name: Session-Token description: Short-lived token returned in the `session_token` field of POST /v3/session/. Used by the hosted verification flow (and custom sandbox UIs) to call session-scoped endpoints on behalf of the verifying user, without your server-side API key.