> ## Documentation Index
> Fetch the complete documentation index at: https://docs.didit.me/llms.txt
> Use this file to discover all available pages before exploring further.

# Document AI API

> Extract the fields **you** define from any supporting document — payslips, proof of funds, source-of-wealth letters, tax statements, contracts, invoices — in one call. Send the file plus a `fields` definition (key, name, extraction instruction, type) and get back typed values (`text`, `number`, `date` as `YYYY-MM-DD`), an optional name-match score against the expected person or company, PDF/EXIF tamper signals, an `Approved`/`Declined`/`In Review` `status`, and a `warnings` list explaining every issue found. This is the server-to-server counterpart of the hosted Document AI workflow step (see `/core-technology/document-ai/overview`); it uses Didit's default extraction model and does not expose provider selection.

**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.

**Request format.** `multipart/form-data` only. `fields` is **one JSON-encoded array string** (1–30 items, at most 16 KB); bracket-style form arrays are rejected with `400`. Each item is `{key, name, instruction, type, required, is_full_name}` — `key` is the JSON key you get back (`^[a-z0-9_]{1,64}$`, unique), `instruction` tells the model what to read, `type` is `text` (default) | `number` | `date`, `required` raises `DOCUMENT_AI_MISSING_REQUIRED_FIELDS` when the value cannot be read, and at most one field may set `is_full_name: true` to mark the value compared against the expected name.

**Name matching.** Pass `expected_first_name` + `expected_last_name` (person) **or** `expected_company_name` (+ optional `expected_company_country`) — not both. The value of the `is_full_name` field (or, when none is marked, the first text field whose key looks like a name) is compared fuzzily; `name_match_score` (0–100) and `name_on_document` are always returned when both sides exist, and a score below `document_ai_name_match_score_threshold` (default 80) adds `DOCUMENT_AI_NAME_MISMATCH`, governed by `document_ai_name_mismatch_action`.

**Decision logic.** Each risk maps to a configurable action: `DECLINE` → `status: "Declined"`, `REVIEW` → `status: "In Review"`, `NO_ACTION` → informational only. Defaults mirror the workflow step: unreadable document `REVIEW`, missing required fields `REVIEW`, suspected manipulation `DECLINE`, name mismatch `NO_ACTION`, unsupported file `REVIEW`. A document that cannot be processed at all returns `400`; a readable but problematic document returns `200` with `Declined`/`In Review` — always inspect `document_ai.status` and `document_ai.warnings`, not just the HTTP code.

**Billing.** Each `200` response consumes one Document AI API credit — **$0.20 per document, no free tier** (standalone APIs never have one). When the organization's balance cannot cover the call, the endpoint returns `403` before any processing. Sandbox API keys return a deterministic mock response and are not charged.

**Persistence.** With `save_api_request=true` (default) the call is stored as an API-type session: the document, the extracted values, the field definitions and the name-match snapshot are kept, the session shows up in the Business Console, and `request_id` works with `GET /v3/session/{sessionId}/decision/`.

Send one document and the list of fields you want read from it. Didit runs its default extraction model over the file, returns every field typed (`text`, `number`, or `date` as `YYYY-MM-DD`), checks the file for manipulation, and optionally compares the name on the document against the person or company you expect. Use it for proof of funds, payslips, bank statements, tax documents, invoices, or any document that does not fit a fixed template.

This is the server-to-server version of the hosted [Document AI](/core-technology/document-ai/overview) step. The request is `multipart/form-data`: the file goes in `document`, and the field definitions go in `fields` as one JSON-encoded array string.

## Define the fields to extract

`fields` is a JSON array of 1 to 30 items, sent as a single string. Each item has a `key` (lowercase letters, digits, and underscores, unique within the request), a human-readable `name`, an `instruction` that tells the model what to look for, a `type` (`text`, `number`, or `date`), and two flags: `required` and `is_full_name`.

```json theme={null}
[
  {
    "key": "account_holder",
    "name": "Account holder",
    "instruction": "Full name of the person or company the account belongs to",
    "type": "text",
    "required": true,
    "is_full_name": true
  },
  {
    "key": "balance",
    "name": "Closing balance",
    "instruction": "Closing balance shown on the statement, as a number without the currency symbol",
    "type": "number",
    "required": true
  },
  {
    "key": "currency",
    "name": "Currency",
    "instruction": "Three-letter currency code of the balance",
    "type": "text"
  },
  {
    "key": "statement_date",
    "name": "Statement date",
    "instruction": "Date the statement was issued",
    "type": "date",
    "required": true
  }
]
```

In a multipart request the whole array is one form value:

```bash theme={null}
curl -X POST https://verification.didit.me/v3/document-ai/ \
  -H "x-api-key: $DIDIT_API_KEY" \
  -F "document=@./bank-statement.pdf" \
  -F 'fields=[{"key":"account_holder","name":"Account holder","instruction":"Full name of the account holder","type":"text","required":true,"is_full_name":true},{"key":"balance","name":"Closing balance","instruction":"Closing balance as a number","type":"number","required":true}]' \
  -F "expected_first_name=Jane" \
  -F "expected_last_name=Doe"
```

The response echoes the normalized `fields` and returns `extracted_data` keyed by your `key` values, with `null` for anything the model could not find. `name`, `instruction`, and `type` are optional on each item (`type` defaults to `text`); `key` is required. Sending `fields` as bracket-style form keys (`fields[0][key]=...`) or as anything other than a JSON array returns `400` before any extraction runs, so nothing is billed.

## Name matching

Mark at most one field with `is_full_name: true` and send who you expect the document to belong to:

| You are verifying | Send                                                                                 | The name is compared against |
| ----------------- | ------------------------------------------------------------------------------------ | ---------------------------- |
| A person          | `expected_first_name` + `expected_last_name`                                         | the person's full name       |
| A company         | `expected_company_name` (+ optional `expected_company_country`, a 2-letter ISO code) | the company name             |

Send one group or the other, not both. When a name is extracted and an expected name is present, the response includes `name_match_score` (0 to 100) and `name_on_document`. A score below `document_ai_name_match_score_threshold` (default `80`) raises the `DOCUMENT_AI_NAME_MISMATCH` warning and applies `document_ai_name_mismatch_action`. If no field is marked `is_full_name`, Didit falls back to the first text field whose key looks like a name (`name`, `holder`, `full_name`, `account_holder`); mark the field explicitly when you want to control which value is compared. When no name value can be found on the document, or no expected name is sent, the name check is skipped and `name_match_score` is `null`.

## Risks and actions

Each risk below maps to an action parameter you can set per request: `DECLINE`, `REVIEW`, or `NO_ACTION`. The strictest triggered action decides `document_ai.status` (`Declined` > `In Review` > `Approved`), and every triggered risk is listed in `document_ai.warnings`. The defaults match the hosted workflow step; see [Document AI rules & warnings](/core-technology/document-ai/warnings-document-ai) for how each risk is detected.

| Risk                                  | Parameter                                    | Default     |
| ------------------------------------- | -------------------------------------------- | ----------- |
| `DOCUMENT_AI_UNREADABLE_DOCUMENT`     | `document_ai_unreadable_document_action`     | `REVIEW`    |
| `DOCUMENT_AI_MISSING_REQUIRED_FIELDS` | `document_ai_missing_required_fields_action` | `REVIEW`    |
| `DOCUMENT_AI_SUSPECTED_MANIPULATION`  | `document_ai_document_tampering_action`      | `DECLINE`   |
| `DOCUMENT_AI_NAME_MISMATCH`           | `document_ai_name_mismatch_action`           | `NO_ACTION` |
| `DOCUMENT_AI_UNSUPPORTED_FILE`        | `document_ai_unsupported_file_action`        | `REVIEW`    |

## Files, saving, and billing

* **Accepted files:** PDF (multi-page supported), JPG, JPEG, PNG, TIFF, WebP, or a ZIP containing the document, up to 30 MB. Encrypted PDFs return `400`; upload a decrypted PDF or a photo instead.
* **`save_api_request`** (default `true`): the request and its result appear in the Business Console under **Manual Checks**, where your team can review them. Set it to `false` to skip saving.
* **`vendor_data`** and **`metadata`** are returned unchanged in the response so you can link the result to your own records.
* **Price:** \$0.20 per document, billed when the document is processed and a status is returned. Standalone APIs do not include the free monthly workflow tier. See [Pricing](/getting-started/pricing#standalone-api-pricing-server-to-server).

<Note>
  Need the user to upload the document themselves, collect up to 3 documents in one flow, auto-translate the upload screen, or cross-reference extracted fields against ID or questionnaire data? Use the hosted [Document AI workflow step](/core-technology/document-ai/overview) instead.
</Note>


## OpenAPI

````yaml POST /v3/document-ai/
openapi: 3.0.0
info:
  version: 3.0.0
  title: Didit Verification API
  description: Identity verification API. Authenticate with x-api-key header.
servers:
  - url: https://verification.didit.me
security: []
tags: []
paths:
  /v3/document-ai/:
    post:
      tags:
        - Standalone APIs
      summary: Document AI (custom field extraction + tamper checks)
      description: >-
        Extract the fields **you** define from any supporting document —
        payslips, proof of funds, source-of-wealth letters, tax statements,
        contracts, invoices — in one call. Send the file plus a `fields`
        definition (key, name, extraction instruction, type) and get back typed
        values (`text`, `number`, `date` as `YYYY-MM-DD`), an optional
        name-match score against the expected person or company, PDF/EXIF tamper
        signals, an `Approved`/`Declined`/`In Review` `status`, and a `warnings`
        list explaining every issue found. This is the server-to-server
        counterpart of the hosted Document AI workflow step (see
        `/core-technology/document-ai/overview`); it uses Didit's default
        extraction model and does not expose provider selection.


        **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.


        **Request format.** `multipart/form-data` only. `fields` is **one
        JSON-encoded array string** (1–30 items, at most 16 KB); bracket-style
        form arrays are rejected with `400`. Each item is `{key, name,
        instruction, type, required, is_full_name}` — `key` is the JSON key you
        get back (`^[a-z0-9_]{1,64}$`, unique), `instruction` tells the model
        what to read, `type` is `text` (default) | `number` | `date`, `required`
        raises `DOCUMENT_AI_MISSING_REQUIRED_FIELDS` when the value cannot be
        read, and at most one field may set `is_full_name: true` to mark the
        value compared against the expected name.


        **Name matching.** Pass `expected_first_name` + `expected_last_name`
        (person) **or** `expected_company_name` (+ optional
        `expected_company_country`) — not both. The value of the `is_full_name`
        field (or, when none is marked, the first text field whose key looks
        like a name) is compared fuzzily; `name_match_score` (0–100) and
        `name_on_document` are always returned when both sides exist, and a
        score below `document_ai_name_match_score_threshold` (default 80) adds
        `DOCUMENT_AI_NAME_MISMATCH`, governed by
        `document_ai_name_mismatch_action`.


        **Decision logic.** Each risk maps to a configurable action: `DECLINE` →
        `status: "Declined"`, `REVIEW` → `status: "In Review"`, `NO_ACTION` →
        informational only. Defaults mirror the workflow step: unreadable
        document `REVIEW`, missing required fields `REVIEW`, suspected
        manipulation `DECLINE`, name mismatch `NO_ACTION`, unsupported file
        `REVIEW`. A document that cannot be processed at all returns `400`; a
        readable but problematic document returns `200` with `Declined`/`In
        Review` — always inspect `document_ai.status` and
        `document_ai.warnings`, not just the HTTP code.


        **Billing.** Each `200` response consumes one Document AI API credit —
        **$0.20 per document, no free tier** (standalone APIs never have one).
        When the organization's balance cannot cover the call, the endpoint
        returns `403` before any processing. Sandbox API keys return a
        deterministic mock response and are not charged.


        **Persistence.** With `save_api_request=true` (default) the call is
        stored as an API-type session: the document, the extracted values, the
        field definitions and the name-match snapshot are kept, the session
        shows up in the Business Console, and `request_id` works with `GET
        /v3/session/{sessionId}/decision/`.
      operationId: post_v3document-ai
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - document
                - fields
              properties:
                document:
                  type: string
                  format: binary
                  description: >-
                    The document to read. Allowed extensions: `tiff`, `jpg`,
                    `jpeg`, `png`, `pdf`, `webp`, `zip`. Maximum upload size:
                    **30 MB** (larger files are rejected with `400`). Multi-page
                    PDFs are supported (expect higher latency); encrypted PDFs
                    are rejected with `400`. Images are automatically compressed
                    before processing.
                fields:
                  type: string
                  description: >-
                    One JSON-encoded array string with **1–30** field
                    definitions (maximum 16 KB). Each item: `key` (required,
                    `^[a-z0-9_]{1,64}$`, unique — the key returned in
                    `extracted_data`), `name` (≤100 chars, shown in messages),
                    `instruction` (≤500 chars, what the model should read),
                    `type` (`text` default | `number` | `date`), `required`
                    (boolean, default `false`), `is_full_name` (boolean, default
                    `false`, at most one per request). Bracket-style form
                    arrays, invalid JSON, duplicate keys, or more than one
                    `is_full_name` return `400` before any processing or
                    billing.
                  example: >-
                    [{"key": "employer_name", "name": "Employer name",
                    "instruction": "The employer or company issuing the
                    payslip.", "type": "text", "required": true, "is_full_name":
                    false}, {"key": "employee_name", "name": "Employee name",
                    "instruction": "The full name of the employee as printed on
                    the payslip.", "type": "text", "required": true,
                    "is_full_name": true}, {"key": "net_pay", "name": "Net pay",
                    "instruction": "The net amount paid to the employee for the
                    period.", "type": "number", "required": true,
                    "is_full_name": false}, {"key": "pay_date", "name": "Pay
                    date", "instruction": "The payment date shown on the
                    payslip.", "type": "date", "required": false,
                    "is_full_name": false}]
                expected_first_name:
                  type: string
                  description: >-
                    First name to verify against the document's name field
                    (fuzzy match, transliteration-aware). Use together with
                    `expected_last_name`; cannot be combined with
                    `expected_company_name`.
                  example: Sophia
                expected_last_name:
                  type: string
                  description: >-
                    Last name to verify against the document's name field,
                    combined with `expected_first_name` for `name_match_score`.
                  example: Martinez
                expected_company_name:
                  type: string
                  description: >-
                    Company name to verify against the document's name field
                    (for business documents). Cannot be combined with
                    `expected_first_name` / `expected_last_name` (no example
                    value on purpose — the generated request sample uses the
                    person fields; swap them for this one when verifying a
                    business document).
                expected_company_country:
                  type: string
                  description: >-
                    Optional ISO 3166-1 alpha-2 country code (two ASCII letters,
                    e.g. `FR`) that tunes company-name normalization for
                    `expected_company_name`. Only used with
                    `expected_company_name`.
                document_ai_name_match_score_threshold:
                  type: integer
                  minimum: 0
                  maximum: 100
                  default: 80
                  description: >-
                    Minimum `name_match_score` (0–100) to consider the name
                    matched. A lower score adds `DOCUMENT_AI_NAME_MISMATCH`,
                    governed by `document_ai_name_mismatch_action`.
                  example: 80
                document_ai_unreadable_document_action:
                  type: string
                  enum:
                    - DECLINE
                    - REVIEW
                    - NO_ACTION
                  default: REVIEW
                  description: >-
                    What to do when no field could be read from the document
                    (`DOCUMENT_AI_UNREADABLE_DOCUMENT`).
                document_ai_missing_required_fields_action:
                  type: string
                  enum:
                    - DECLINE
                    - REVIEW
                    - NO_ACTION
                  default: REVIEW
                  description: >-
                    What to do when at least one `required` field could not be
                    read (`DOCUMENT_AI_MISSING_REQUIRED_FIELDS`;
                    `additional_data.missing_fields` lists them).
                document_ai_document_tampering_action:
                  type: string
                  enum:
                    - DECLINE
                    - REVIEW
                    - NO_ACTION
                  default: DECLINE
                  description: >-
                    What to do when PDF/EXIF forensics suspect manipulation —
                    modified after digital signing, known PDF editor,
                    overlay-text manipulation, suspicious re-export, image
                    anomalies (`DOCUMENT_AI_SUSPECTED_MANIPULATION`).
                document_ai_name_mismatch_action:
                  type: string
                  enum:
                    - DECLINE
                    - REVIEW
                    - NO_ACTION
                  default: NO_ACTION
                  description: >-
                    What to do when `name_match_score` is below the threshold
                    (`DOCUMENT_AI_NAME_MISMATCH`).
                document_ai_unsupported_file_action:
                  type: string
                  enum:
                    - DECLINE
                    - REVIEW
                    - NO_ACTION
                  default: REVIEW
                  description: >-
                    What to do when the upload is empty or not a processable
                    file (`DOCUMENT_AI_UNSUPPORTED_FILE`).
                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`, with the uploaded document, the extracted
                    values, the field definitions and the name-match snapshot
                    stored. When `false`, nothing is stored and `request_id` is
                    a transient UUID for response correlation only; the call is
                    still billed.
                  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
      responses:
        '200':
          description: >-
            Document processed. `document_ai.status` is `Approved`, `Declined`,
            or `In Review`; every detected issue is itemized in
            `document_ai.warnings`. A problematic document still returns `200` —
            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: Payslip read cleanly, name matched
                  value:
                    request_id: 9d6a3b1e-2f0c-4a8e-9b7d-1c2e3f4a5b6c
                    document_ai:
                      status: Approved
                      extracted_data:
                        employer_name: Acme Payments Ltd
                        employee_name: Sophia Martinez
                        net_pay: 3120.5
                        pay_date: '2026-07-31'
                      fields:
                        - key: employer_name
                          name: Employer name
                          instruction: The employer or company issuing the payslip.
                          type: text
                          required: true
                          is_full_name: false
                        - key: employee_name
                          name: Employee name
                          instruction: >-
                            The full name of the employee as printed on the
                            payslip.
                          type: text
                          required: true
                          is_full_name: true
                        - key: net_pay
                          name: Net pay
                          instruction: The net amount paid to the employee for the period.
                          type: number
                          required: true
                          is_full_name: false
                        - key: pay_date
                          name: Pay date
                          instruction: The payment date shown on the payslip.
                          type: date
                          required: false
                          is_full_name: false
                      name_match_score: 100
                      name_on_document: Sophia Martinez
                      warnings: []
                    vendor_data: user-123
                    metadata:
                      flow: onboarding
                    created_at: '2026-08-19T16:40:12.318Z'
                In Review — missing required field:
                  summary: '`net_pay` could not be read; default action REVIEW'
                  value:
                    request_id: 3f2a9c7b-5d4e-4b1a-8c6f-0e9d8c7b6a5f
                    document_ai:
                      status: In Review
                      extracted_data:
                        employer_name: Acme Payments Ltd
                        employee_name: Sophia Martinez
                        net_pay: null
                        pay_date: '2026-07-31'
                      fields:
                        - key: employer_name
                          name: Employer name
                          instruction: The employer or company issuing the payslip.
                          type: text
                          required: true
                          is_full_name: false
                        - key: employee_name
                          name: Employee name
                          instruction: >-
                            The full name of the employee as printed on the
                            payslip.
                          type: text
                          required: true
                          is_full_name: true
                        - key: net_pay
                          name: Net pay
                          instruction: The net amount paid to the employee for the period.
                          type: number
                          required: true
                          is_full_name: false
                        - key: pay_date
                          name: Pay date
                          instruction: The payment date shown on the payslip.
                          type: date
                          required: false
                          is_full_name: false
                      name_match_score: 100
                      name_on_document: Sophia Martinez
                      warnings:
                        - risk: DOCUMENT_AI_MISSING_REQUIRED_FIELDS
                          feature: DOCUMENT_AI
                          additional_data:
                            missing_fields:
                              - Net pay
                          log_type: warning
                          short_description: Required fields could not be extracted
                          long_description: >-
                            One or more fields configured as required could not
                            be extracted from the uploaded document.
                    vendor_data: null
                    metadata: null
                    created_at: '2026-08-19T16:41:05.002Z'
                Declined — suspected manipulation:
                  summary: PDF forensics flagged a known editor; default action DECLINE
                  value:
                    request_id: 7c1d2e3f-4a5b-4c6d-8e9f-0a1b2c3d4e5f
                    document_ai:
                      status: Declined
                      extracted_data:
                        employer_name: Acme Payments Ltd
                        employee_name: Sophia Martinez
                        net_pay: 3120.5
                        pay_date: '2026-07-31'
                      fields:
                        - key: employer_name
                          name: Employer name
                          instruction: The employer or company issuing the payslip.
                          type: text
                          required: true
                          is_full_name: false
                        - key: employee_name
                          name: Employee name
                          instruction: >-
                            The full name of the employee as printed on the
                            payslip.
                          type: text
                          required: true
                          is_full_name: true
                        - key: net_pay
                          name: Net pay
                          instruction: The net amount paid to the employee for the period.
                          type: number
                          required: true
                          is_full_name: false
                        - key: pay_date
                          name: Pay date
                          instruction: The payment date shown on the payslip.
                          type: date
                          required: false
                          is_full_name: false
                      name_match_score: null
                      name_on_document: Sophia Martinez
                      warnings:
                        - risk: DOCUMENT_AI_SUSPECTED_MANIPULATION
                          feature: DOCUMENT_AI
                          additional_data:
                            detection_method: known_editor
                            processed_by_known_editor: ilovepdf
                          log_type: error
                          short_description: Suspected document manipulation
                          long_description: >-
                            The system detected signs of potential manipulation
                            or editing in the uploaded document.
                    vendor_data: null
                    metadata: null
                    created_at: '2026-08-19T16:42:40.771Z'
              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.
                  document_ai:
                    type: object
                    properties:
                      status:
                        type: string
                        enum:
                          - Approved
                          - Declined
                          - In Review
                        description: >-
                          `Approved` when no warning resolves to a decline or
                          review; `Declined` when at least one risk maps to
                          `DECLINE`; otherwise `In Review` when at least one
                          maps to `REVIEW`. The `warnings` list explains why.
                      extracted_data:
                        type: object
                        additionalProperties: true
                        description: >-
                          One entry per requested field key. Values are typed
                          per the field `type` — strings for `text`, numbers for
                          `number`, `YYYY-MM-DD` strings for `date` — or `null`
                          when the value could not be read.
                        example:
                          employer_name: Acme Payments Ltd
                          employee_name: Sophia Martinez
                          net_pay: 3120.5
                          pay_date: '2026-07-31'
                      fields:
                        type: array
                        description: >-
                          The normalized field definitions that were used for
                          this extraction, echoed back (also stored with the
                          session when `save_api_request=true`).
                        items:
                          type: object
                          properties:
                            key:
                              type: string
                            name:
                              type: string
                            instruction:
                              type: string
                            type:
                              type: string
                              enum:
                                - text
                                - number
                                - date
                            required:
                              type: boolean
                            is_full_name:
                              type: boolean
                      name_match_score:
                        type: number
                        nullable: true
                        minimum: 0
                        maximum: 100
                        description: >-
                          Fuzzy similarity (0–100) between `name_on_document`
                          and the expected person/company name. Returned
                          whenever both a name value and an expected name exist
                          — on matches **and** mismatches; `null` otherwise.
                        example: 96
                      name_on_document:
                        type: string
                        nullable: true
                        description: >-
                          The name value taken from the document (the
                          `is_full_name` field, or the first name-like text
                          field) that was compared against the expected name.
                          `null` when no name value was available.
                        example: Sophia Martinez
                      warnings:
                        type: array
                        description: >-
                          Empty on a clean approval. Every entry explains one
                          detected risk; entries with `log_type: "error"` set
                          `status` to `Declined`, entries with `log_type:
                          "warning"` set it to `In Review`.
                        items:
                          type: object
                          properties:
                            risk:
                              type: string
                              enum:
                                - DOCUMENT_AI_UNREADABLE_DOCUMENT
                                - DOCUMENT_AI_MISSING_REQUIRED_FIELDS
                                - DOCUMENT_AI_SUSPECTED_MANIPULATION
                                - DOCUMENT_AI_NAME_MISMATCH
                                - DOCUMENT_AI_UNSUPPORTED_FILE
                              description: >-
                                Machine-readable risk code. Each one is governed
                                by the matching `document_ai_*_action` request
                                option.
                              example: DOCUMENT_AI_MISSING_REQUIRED_FIELDS
                            feature:
                              type: string
                              enum:
                                - DOCUMENT_AI
                              description: >-
                                Feature that raised the warning. Always
                                `DOCUMENT_AI` on this endpoint.
                            additional_data:
                              type: object
                              nullable: true
                              additionalProperties: true
                              description: >-
                                Extra context for the risk: `{"missing_fields":
                                ["Net pay"]}` for missing required fields,
                                `{"name_on_document", "score", "threshold"}` for
                                a name mismatch, and the forensic finding
                                (`detection_method`, producer/editor details)
                                for suspected manipulation. `null` when there is
                                nothing to add.
                              example:
                                missing_fields:
                                  - Net pay
                            log_type:
                              type: string
                              enum:
                                - error
                                - warning
                                - information
                              description: >-
                                Severity derived from the configured action:
                                `error` (DECLINE), `warning` (REVIEW),
                                `information` (NO_ACTION).
                            short_description:
                              type: string
                              description: Human-readable one-liner for the risk.
                              example: Required fields could not be extracted
                            long_description:
                              type: string
                              description: >-
                                Human-readable explanation, interpolated with
                                `additional_data` where relevant.
                  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: When the request was processed (UTC, ISO 8601).
        '400':
          description: >-
            Validation or processing error. Field-level problems return DRF's
            standard envelope (one array of messages per offending field).
            Encrypted or unreadable documents and extraction failures return
            `{"error": ...}`.
          content:
            application/json:
              examples:
                Missing document:
                  summary: '`document` not included in the form data'
                  value:
                    document:
                      - No file was submitted.
                Invalid fields payload:
                  summary: '`fields` is not one JSON-encoded array string'
                  value:
                    fields:
                      - fields must be a valid JSON array string.
                Duplicate field keys:
                  summary: Two items share the same `key`
                  value:
                    fields:
                      - Every fields item must have a unique key.
                Conflicting expectations:
                  summary: Person and company expectations sent together
                  value:
                    non_field_errors:
                      - >-
                        Provide either person expected-name fields or
                        expected_company_name, not both.
                Invalid metadata:
                  summary: >-
                    `metadata` is not a JSON object (or contains a NUL
                    character)
                  value:
                    metadata:
                      - Metadata must be a JSON object.
                Oversized vendor_data:
                  summary: '`vendor_data` over the 2,000-byte cap'
                  value:
                    vendor_data:
                      - vendor_data is too long (maximum 2000 bytes).
                Encrypted PDF:
                  summary: Password-protected PDF
                  value:
                    error: >-
                      The PDF is encrypted. Please upload a decrypted PDF or a
                      photo instead.
                Extraction failed:
                  summary: The extraction pipeline could not process the document
                  value:
                    error:
                      - Error extracting Document AI information
              schema:
                type: object
                additionalProperties: true
        '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 — and contract-billed
            organizations whose rate card has no Document AI API entry — 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
                Contract rate missing:
                  summary: >-
                    Contract-billed organization whose rate card has no Document
                    AI API entry
                  value:
                    error: >-
                      Document AI API is not included in your contract rate
                      card. Contact support@didit.me to enable it.
              schema:
                type: object
                properties:
                  detail:
                    type: string
                  error:
                    type: string
        '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:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key

````