---
title: "Upload supporting-document bytes"
method: PUT
path: "/customers/{customer_id}/documents/{document_id}/upload"
tags: ["Customers"]
---

# Upload supporting-document bytes

`PUT /customers/{customer_id}/documents/{document_id}/upload`

## Request

- Base URL: `https://developers.ozav.io/v2`
- URL: `https://developers.ozav.io/v2/customers/{customer_id}/documents/{document_id}/upload`
- Auth: one of:
  - API key in header `X-API-Key`
  - HTTP bearer

Uploads file bytes to the short-lived OZAV-hosted `upload_url`
returned by `POST /customers/{customer_id}/documents`. Send the same
API key used to create the grant. OZAV stores the file in private
storage, recomputes size and SHA-256 server-side, and moves the
document to `received` when the bytes are accepted.

## Query parameters

- `upload_token` string, required

## Request body `application/pdf`

- string, binary

## Response `200`

Document bytes accepted and the document is marked received.

- CustomerDocument: A customer's uploaded supporting document. Never exposes the raw storage location.
  - `id` string: Opaque document id.
  - `object` 'document': Resource type discriminator; always the literal `document`.
  - `customer_id` string: The customer this document belongs to.
  - `subject_type` 'customer' | 'associated_person': Whose document this is. `customer` - the customer itself. `associated_person` - one of a business customer's associated persons, named in `associated_person_id`.
  - `associated_person_id` string: Present when subject_type is associated_person.
  - `doc_type` 'identity_document' | 'selfie' | 'liveness' | 'proof_of_address' | 'company_registration' | 'ownership_structure' | 'financial_statement' | 'source_of_funds' | 'bank_statement' | 'authorized_signature_form' | 'banking_reference' | 'corporate_resolution' | 'good_standing_certificate' | 'source_of_funds_certification' | 'tax_registration' | 'pep_declaration' | 'income_tax_return' | 'other': The kind of KYC/KYB document. Jurisdiction-agnostic - generic categories, never country-specific artifact names - because this API onboards your customers worldwide. **What this list is, and what it is not.** It is the vocabulary for declaring what you are uploading. It is not a per-country list of which artifact satisfies which category: whether a given national ID, driving licence or utility bill is accepted for a specific customer is decided in review, not by this enum, and OZAV does not publish a per-jurisdiction acceptance table. Declare the category that best describes the artifact and upload it; if something is missing or unusable you get a named outcome on the verification, not a silent rejection. The fields a customer must supply are a separate question, answered by `requirements.missing_fields` on the customer itself. That set is driven by the country on `tax_id` / `registration`, and it does not vary by the currency of any account you later open. **Required versus optional, and why nothing here blocks.** For an international account the receiving institution asks for a defined set of company information. Some categories below satisfy part of that set and some do not apply to every company - a Brazilian company, for example, has no separate memorandum of association, because its Contrato Social is both the constitutive act and the articles. So this API accepts every category at any time and refuses none of them for being absent. Upload what the company has, under the category that describes it. **What this API does not do yet, said plainly.** `requirements` on `GET /customers/{id}` reports missing onboarding *fields*, not missing documents - it will not tell you which of the categories above are still outstanding. Document review happens at the receiving institution, and today its outcome reaches you through your account contact, not through this API. Publishing the list here without saying that would suggest a completeness signal that does not exist. A company that cannot produce one of these is not blocked from being created. It is blocked, later and visibly, from having an account opened - and the difference matters: the first is a wall at the door, the second is a list you can work through. **Three categories for the Brazilian named account.** `tax_registration` is the company's tax registration certificate (in Brazil, the Cartão CNPJ). `pep_declaration` is a politically-exposed-person declaration, one per partner or legal representative (`subject_type: associated_person`), optional except for a BRL account. `income_tax_return` is the company's income tax return, accepted as evidence of economic capacity alongside `financial_statement`. A bank statement is **not** accepted as economic capacity by the Brazilian receiving institution; send a balance sheet or the tax return instead.
  - `side` 'front' | 'back' | 'single' | 'null', nullable: Which face of a two-sided document these bytes are. `single` for a document captured in one image. `null` where the document type has no sides (a PDF statement).
  - `issuing_country` string, nullable: ISO 3166-1 alpha-2 country that issued the document, when known. `null` when the document type has no issuer (a proof of address) or it was not supplied.
  - `status` 'pending_upload' | 'received' | 'rejected': `pending_upload` = grant issued, bytes not yet confirmed; `received` = bytes present; `rejected` = failed review.
  - `mime` string, nullable: Content type of the stored bytes, as observed on upload. `null` while `status` is `pending_upload`.
  - `size_bytes` integer, nullable: Size of the stored bytes. `null` while `status` is `pending_upload`.
  - `sha256` string, nullable: SHA-256 of the stored bytes, computed by OZAV on receipt - not a value you send. Compare it against your own digest to prove the upload was not truncated. `null` while `status` is `pending_upload`.
  - `review` DocumentReview: OZAV's own review of a supporting document (proof of address, bank statement, proof of income, company document). The identity document and the selfie are read by OZAV too (the selfie only for a face being present, not liveness or a match with the document); liveness and face match remain a hosted-session result. `rejected` comes with a closed `reason_code` and its English label; a fresh `requirements.documents_due` line follows with a new deadline. The `document.reviewed` webhook carries the same outcome. Omitted (not `not_reviewed`) when the review could not be read at response time.
    - `status` 'accepted' | 'rejected' | 'in_review' | 'not_reviewed', required: - `not_reviewed` - no review yet (or not a document OZAV reviews). - `in_review` - waiting on a compliance officer. - `accepted` - the document satisfies the requirement. - `rejected` - refused; see `reason_code`. Send a new file.
    - `reason_code` 'unreadable' | 'not_the_requested_document' | 'holder_mismatch' | 'holder_is_not_the_company' | 'holder_in_other_script' | 'expired' | 'address_mismatch' | 'low_confidence' | 'tampering_signals' | 'missing_period' | 'not_registered' | 'registry_mismatch' | 'outdated_version' | 'ownership_not_evidenced' | 'holder_shares_address' | 'officer_decision': Why a document was rejected. Closed vocabulary; each code has a fixed English label in `review.reason`.
    - `reason` string: English label of `reason_code`. Present only with `reason_code`.
    - `reviewed_at` string, date-time: When the current review was recorded. Absent while `not_reviewed`.
  - `created_at` string, date-time: When the upload grant was created (RFC 3339, UTC), not when bytes arrived.
  - `updated_at` string, date-time: When the record last changed (RFC 3339, UTC) - the receipt of bytes and a review outcome both move it.

## Other responses

- `400`: The request is invalid.
- `401`: Missing or invalid API key.
- `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.
- `404`: The resource does not exist, is not visible to the caller, or the id was malformed for this position (see "Resource ids" under Errors). These share one status; `code` and `message` tell them apart.
- `409`: Conflict (e.g. IDEMPOTENCY_KEY_REUSED).

---

[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)
