---
title: "Analyze one document"
method: POST
path: "/documents/analyze"
tags: ["Screenings"]
---

# Analyze one document

`POST /documents/analyze`

## Request

- Base URL: `https://developers.ozav.io/v2`
- URL: `https://developers.ozav.io/v2/documents/analyze`
- Auth: one of:
  - API key in header `X-API-Key`
  - HTTP bearer

Reads one document and returns a verdict, its reason, what was compared and the fields read,
with no customer and no stored record. The same reader and rules OZAV uses on its own
onboarding documents.

Send the file as base64 (up to 3 MB; PDF, JPEG, PNG or WebP) with its `document_type`. Send
`subject` to compare the holder and the address against who the document should belong to;
without it, `comparisons` say `not_compared` and the verdict covers the rest (legibility, the
right kind of document, its age, signs of editing).

- `verdict: accepted` - the document is what was asked and nothing holds it.
- `verdict: review` - a person should look; `reason` says why.
- `verdict: rejected` - unreadable, the wrong kind of document, or too old for its purpose.

Every verdict, `rejected` included, is a real answer and is charged. A file that cannot be
analyzed (empty, too large, content not matching `mime_type`, password-protected PDF) is
`400 INVALID_FILE`. When the document cannot be analyzed right now, the answer is
`503 DOCUMENT_ANALYSIS_UNAVAILABLE`: it says nothing about the document. The charge for
either is refunded.

Nothing about the document is stored, so this operation takes no `Idempotency-Key`: a retry
is a new analysis.

## Request body

- DocumentAnalysisRequest
  - `document_type` 'proof_of_address' | 'bank_statement' | 'tax_return' | 'financial_statement' | 'incorporation_articles' | 'shareholder_registry' | 'directors_registry' | 'tax_registration' | 'identity_document', required: What the document should be. The same vocabulary as the customer document upload. `identity_document` reads a national ID card, a driver's license, a passport, a residence card or a foreign national ID: the type, the printed holder data and the expiry date, compared with `subject`. It does not check authenticity (templates, holograms, the machine-readable zone, screen captures) and the answer says so with `authenticity_checked: false`.
  - `file` object, required
    - `content_base64` string, required: The file, base64-encoded. Up to 3 MB before encoding.
    - `mime_type` 'application/pdf' | 'image/jpeg' | 'image/png' | 'image/webp', required
  - `subject` object: Who the document should belong to. Optional; without it nothing is compared.
    - `type` 'individual' | 'business', required
    - `name` string
    - `registration_number` string: Tax id (CPF or CNPJ in Brazil).
    - `date_of_birth` string, date: YYYY-MM-DD. Read only for `identity_document`, compared with the date printed on it.
    - `country` string: ISO 3166-1 alpha-2.
    - `address` object
      - `street` string
      - `city` string
      - `state` string
      - `postal_code` string

## Response `200`

The analysis.

- DocumentAnalysis
  - `object` 'document_analysis', required
  - `document_type` string, required
  - `verdict` 'accepted' | 'review' | 'rejected', required
  - `reason` object, nullable, required: Why the verdict is not a plain `accepted`. `null` when nothing held it.
    - `code` string, required: Stable machine code, a plain string (new codes are added over time). Known values: `unreadable`, `not_the_requested_document`, `holder_mismatch`, `holder_in_other_script`, `holder_is_not_the_company`, `holder_shares_address`, `expired`, `address_mismatch`, `low_confidence`, `tampering_signals`, `missing_period`, `not_registered`, `registry_mismatch`, `outdated_version`, `ownership_not_evidenced`, `expiry_not_read`, `holder_not_compared`.
    - `description` string, required
  - `confidence` number, nullable, required: How confident the reading is. `null` for `identity_document`, whose reading has no score.
  - `authenticity_checked` boolean: Only on `identity_document`, always `false`: the printed data is read and compared, the document is not checked for genuineness. An `accepted` means legible, of an accepted type, in date, and, when you sent the holder, at least one field matched and none differed.
  - `comparisons` object, required
    - `tax_id` 'match' | 'mismatch' | 'not_compared': Only on `identity_document`: the CPF printed on it against `subject.registration_number`.
    - `date_of_birth` 'match' | 'mismatch' | 'not_compared': Only on `identity_document`: the birth date printed on it against `subject.date_of_birth`.
    - `holder` 'match' | 'partial' | 'mismatch' | 'not_compared', required
    - `address` 'match' | 'mismatch' | 'not_compared', required
    - `document_age_days` integer, nullable, required: Days since the document date; `null` when no date was read.
  - `extracted` object, required: What was read from the document. A field that was not read is `null`, never guessed.
    - `legible` boolean
    - `is_requested_type` boolean
    - `holder_name` string, nullable
    - `holders` string[]: Every holder printed on the document (joint accounts).
    - `holder_tax_id` string, nullable: Digits only.
    - `id_type` string, nullable: Only on `identity_document`: the type read, as a stable code (new codes may be added).
    - `document_number` string, nullable: Only on `identity_document`.
    - `date_of_birth` string, date, nullable: Only on `identity_document`.
    - `issuing_country` string, nullable: Only on `identity_document`. ISO 3166-1 alpha-2.
    - `expiry_date` string, date, nullable: Only on `identity_document`. `null` when none is printed (old RG): no date is not expired.
    - `address` object, nullable
      - `street` string, nullable
      - `city` string, nullable
      - `state` string, nullable
      - `postal_code` string, nullable
    - `issuer` string, nullable
    - `reference_date` string, date, nullable: Issue date, or the end of the period of a statement.
    - `period_start` string, date, nullable
    - `tampering_detected` boolean: Visible signs of editing were seen.
  - `rule_version` string, required

## Other responses

- `400`: The request is invalid.
- `401`: Missing or invalid API key.
- `402`: OZAV Services only (prepaid): your prepaid credit does not cover this call (code INSUFFICIENT_CREDIT). The call did not run and nothing was charged. `details` carries `balance_usd` and `price_usd`; top up in the portal (Billing) and retry. Organizations on the OZAV platform are invoiced monthly and never receive this. On `POST /decisions` and `POST /screenings`, where OZAV accepts x402 payment, the response also carries an x402 v2 challenge: the `PAYMENT-REQUIRED` header (base64 `PaymentRequired`) and the same object in `details.x402` - the price of this one call in native USDC, paid to OZAV. `accepts` lists one entry per network OZAV receives on, Polygon (`eip155:137`) first; where enabled, a second entry offers Base (`eip155:8453`). Pay on any one of them, signing for that network's USDC (the entry's `asset`); a payment whose `accepted.asset` is not the USDC of its `network` is refused with `network_or_asset_not_accepted`. Retry with the `PAYMENT-SIGNATURE` header to pay it. A payment OZAV does not accept answers `402 X402_PAYMENT_REJECTED` with `details.reason` (`amount_below_price`, `wrong_pay_to`, `network_or_asset_not_accepted`, `authorization_not_yet_valid`, `authorization_expires_too_soon`, `authorization_window_too_long`, `signature_invalid`, `authorization_already_used`, `insufficient_balance`, `settlement_refused`, `payment_malformed` or `payment_not_accepted`) and a new challenge; nothing was charged. If OZAV could not confirm the settlement, `503 X402_PAYMENT_UNAVAILABLE`: retry with the same header, it is never charged twice. A retried payment is credited only once OZAV reads, on-chain, the transaction that used the authorization moving the USDC to OZAV; when that cannot be established automatically, the `503` says the payment needs a manual check (contact support with the request id). Where OZAV also accepts MPP, the response carries an MPP challenge in `WWW-Authenticate: Payment` (method `evm`, intent `charge`, credential type `authorization`, valid for five minutes) and in `details.mpp.www_authenticate`. A credential OZAV does not accept answers `402 MPP_PAYMENT_REJECTED` with `details.reason` (the reasons above, plus `challenge_not_issued_by_ozav`, `challenge_expired` and `nonce_not_bound_to_challenge`); `503 MPP_PAYMENT_UNAVAILABLE` means retry with the same credential.
- `403`: The organization lacks the entitlement (code ENTITLEMENT_REQUIRED) or the key lacks the scope (code INSUFFICIENT_SCOPE). On money-moving requests (POST /transfers, POST /wallets/{wallet_id}/transfers) a compliance hold on the customer answers with code COMPLIANCE_HOLD: the hold is reviewed and lifted on OZAV's side; retrying does not clear it. When the only hold is a pause until pending onboarding documents are received, a withdrawal answers CUSTOMER_DOCUMENTS_PENDING instead: send what `requirements` on GET /customers/{id} lists; OZAV lifts the pause after reviewing it. On OZAV Services (live without the platform's KYB), any API outside decisions, screenings, lists and self-custody wallets answers with code PRODUCTION_NOT_ACTIVATED.
- `503`: A dependency OZAV needs (storage, a resource lookup) could not be reached, so OZAV fails closed rather than guess. Safe to retry with the same `Idempotency-Key` where the operation accepts one.

---

[API](https://skmtc.dev/ozav/apis/ozav-api.md) - [All operations](https://skmtc.dev/ozav/apis/ozav-api/llms.txt) - [OpenAPI document](https://skmtc.dev/ozav/apis/ozav-api/revisions/4133dc2109d7?raw)
