---
title: "Start a supporting-document upload for a customer"
method: POST
path: "/customers/{customer_id}/documents"
tags: ["Customers"]
---

# Start a supporting-document upload for a customer

`POST /customers/{customer_id}/documents`

## Request

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

Declares a KYC/KYB document and returns a short-lived
`upload_url` the client PUTs the file bytes to. Documents may belong to
the customer itself or to one of its associated persons. Use generic
document categories such as `company_registration`, `identity_document`,
`selfie`, `liveness`, `proof_of_address`, `ownership_structure`,
`financial_statement`, and `source_of_funds`.

## Request body

- CustomerDocumentUploadRequest
  - `subject_type` 'customer' | 'associated_person': Who this document belongs to. Omit for customer-scoped documents.
  - `associated_person_id` string: Required when subject_type is associated_person. Opaque `ozav_ap_...` id.
  - `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', required: 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.
  - `label` string: Free-text label describing what this file is, in your own words. Strongly recommended whenever `doc_type` is `other`, because without it the document is indistinguishable from any other `other` in review. It complements `doc_type` and never replaces it: the type is what drives requirements, the label is what a human reads.
  - `side` 'front' | 'back' | 'single': Document side when applicable. For `doc_type: identity_document` the side decides which requirement the file closes: `front` (or omitted) closes the front, `back` closes the back (`government_id_back` in `documents_due`), and `single` - front and back in one image - closes both. Omitting `side` keeps closing only the front, so to satisfy an open back requirement send the back with `side: back`.
  - `issuing_country` string: ISO 3166-1 alpha-2 issuing country when applicable.
  - `mime` string: MIME type of the file to upload (e.g. application/pdf, image/png). Optional integrity hint.
  - `size_bytes` integer: Declared file size in bytes. Optional integrity hint.
  - `sha256` string: Lowercase hex SHA-256 of the file. Optional integrity hint; rejected if malformed.

## Response `201`

Upload grant created; PUT the file bytes to `upload_url`.

- CustomerDocumentUpload: 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.
  - `upload_url` string: Short-lived upload URL - `PUT` the raw file bytes here. Not returned by the list endpoint. **Send your API key on this PUT as well.** The embedded `upload_token` scopes the grant to this one customer/document pair and expires on its own, but it does not replace authentication - a `PUT` without `X-API-Key` (or `Authorization: Bearer`) is rejected with `MISSING_API_KEY`. Because the key is required, **this URL is for your server, not your browser**: do not hand it to an end user's device, which would mean shipping your secret key with it. Upload the bytes from your backend.
  - `upload_expires_at` string, date-time: When `upload_url` expires.

## Other responses

- `400`: The request is invalid.
- `401`: Missing or invalid API key.
- `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.

---

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