---
title: "Create a transfer"
method: POST
path: "/transfers"
tags: ["Transfers"]
---

# Create a transfer

`POST /transfers`

## Request

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

The single money-movement resource. The pair (`source.type`,
`destination.type`) selects the flow - you never call a different
endpoint for on-ramp vs off-ramp:

| source          | destination          | flow            | availability |
|-----------------|----------------------|-----------------|--------------|
| `fiat_payment`  | `payout_destination` | FX (fiat→fiat)  | live         |
| `fiat_payment`  | `wallet`             | on-ramp         | live         |
| `account`       | `wallet`             | on-ramp from the named account's balance | live |
| `wallet`        | `payout_destination` | off-ramp        | live         |
| `wallet`        | `account`            | off-ramp to the customer's own account | live |
| `wallet`        | `wallet`             | wallet transfer | live         |
| `wallet`        | `crypto_address`     | crypto send     | live         |

**Sending to an on-chain address runs on this route.** A `wallet` source
with a `crypto_address` destination is a custody move: OZAV signs from the
source wallet's vault and broadcasts. No quote is involved (it is a
same-asset move, not an exchange), so send `amount` and omit `quote_id`.
It needs the `custody` or the `liquidity` product. The `/wallets` routes need `custody`:
listing wallets (`GET /wallets`), reading a customer's wallet, its deposit addresses,
addresses and policy, and `POST /wallets/{wallet_id}/transfers`. It is subject to the two controls below.

**On-ramp from the account balance.** An `account` source spends the BRL balance of the
customer's BRL named account: Pix payments to the account's key accumulate there, and nothing
converts by itself. An account that cannot fund an order yet gets `422 ACCOUNT_NOT_READY`:
it is not active, its institution cannot send Pix, or its balance could not be read just now.
Converting more than the balance not already committed to an open order gets
`422 ACCOUNT_BALANCE_INSUFFICIENT`. If OZAV pauses this flow, every organization gets
`422 ACCOUNT_FUNDED_ORDERS_NOT_ENABLED` at once: nothing in your request causes it.

Some of these refusals come after the transfer is recorded: it ends `failed`, and a retry
with the same `Idempotency-Key` returns the same refusal. To try again later, send the
request with a new key. An `ACCOUNT_NOT_READY` that persists across a few retries means the
account cannot fund orders yet. Each conversion is one transfer with a quote, like any on-ramp, and the
BRL 1,000 on-ramp floor applies per transfer, not per deposit. A smaller deposit stays in
the account and adds up with the next ones.

In the sandbox, a test key converts from a sandbox account: credit it first with
`POST /accounts/{account_id}/simulate/credit`, then send the transfer. The order runs on the
simulator and the conversion debits the simulated balance.

**Wallet-to-wallet runs here too.** The source wallet must belong to the
`customer_id` on the request - that is whose money moves. The destination
only has to be a wallet in your organization, so you can move value between
two of your customers' wallets. A wallet outside your organization is not
found, and a deactivated wallet does not receive.

The two controls:

- **Managed wallets only.** A self-custody source is refused with
  `422 WALLET_NOT_MANAGED` - OZAV holds no key to sign with. That send
  goes through `POST /wallets/{wallet_id}/transaction-intents`, which
  returns unsigned calldata for the end-user's own key.
- **Travel Rule (BCB Resolution 520)** on any send that leaves custody.
  A `crypto_address` destination without complete Travel Rule data is
  refused with `422 TRAVEL_RULE_REQUIRED`, and `details.missing_fields`
  names each field that is missing. A destination that is an `ozav_wal_`
  id is exempt: the move stays inside custody and both sides are already
  verified.

`POST /wallets/{wallet_id}/transfers` also carries a destination `memo`,
which networks like Stellar need for exchange deposits (an exchange
credits by memo). `POST /transfers` has no `memo` field, so a memo-bearing
send belongs on the wallets surface even after custody lands here.

**Receiving crypto needs nothing from you, and is not a transfer.** Fund an
address from `GET /wallets/{wallet_id}/deposit-addresses` and the credit
lands on the wallet by itself. You see it in two places already:

- `GET /wallets/{wallet_id}/balances` reflects the new amount;
- `GET /transactions` lists the arrival alongside your transfers, in one
  timeline, so you do not have to stitch two listings together.

Creating a transfer with a `crypto_payment` source is refused with
`422 NOT_AVAILABLE`, and that refusal is permanent rather than pending: an
arrival is observed, never requested, so there is nothing for you to create.
The one thing still missing is a *push* notification for it - today you
learn about an arrival by reading, not by being told.

BRL/Pix on-ramp returns transaction-specific Pix payment
instructions on `source.payment_instructions`. BRL/Pix off-ramp
creates the operation only after the customer's stable
funds are reserved; final Pix-out remains gated until settlement rails
are enabled. Other combinations fail closed (`422 RAIL_NOT_AVAILABLE` /
`NOT_AVAILABLE`) until settlement is enabled - never a silent partial
result. See
`x-ozav-transfer-flow-availability` for the machine-readable status.

`quote_id` is required whenever source and target currencies differ, and
is authoritative for the amount when present. For a same-currency move (no
FX, so no quote) provide `amount` - it is how much to move. Exactly one of
`quote_id` or `amount` must be present; supplying both is a 400. The
`Idempotency-Key` header is required.

## Headers

- `Idempotency-Key` string, required

## Request body

- object: Provide `amount` when `quote_id` is omitted (same-currency moves carry the amount here); when `quote_id` is present the quote is authoritative and `amount` is rejected. Exactly one of the two is required.
  - `customer_id` string, required: The `ozav_cus_` id of the customer this transfer is for.
  - `quote_id` string: The `ozav_q_` id of the quote to execute. Required when source and target currencies differ; authoritative for the amount when present.
  - `amount` TransferMonetaryAmount: Amount block accepted on transfer creation (8-fractional-digit precision).
    - `amount` string, required: Decimal string accepted on transfer creation - up to 8 fractional digits. Never a float.
    - `currency` string, required: Uppercase ISO 4217 code or stablecoin symbol (e.g. BRL, USD, EUR, MXN, USDC, USDT). Uppercase is the canonical form: values are accepted case-insensitively on input but are always uppercase in responses.
  - `source` union, required: Where a transfer's funds come from. Discriminated by `type`; the value selects the flow together with the destination.
    - TransferSourceFiatPayment: A fiat payment the customer sends in (the FX / on-ramp source leg).
      - `type` 'fiat_payment', required: Discriminator for a fiat-payment source; always the literal `fiat_payment`.
      - `currency` string, required: Uppercase ISO 4217 code or stablecoin symbol (e.g. BRL, USD, EUR, MXN, USDC, USDT). Uppercase is the canonical form: values are accepted case-insensitively on input but are always uppercase in responses.
      - `rail` 'pix' | 'ach' | 'fedwire' | 'sepa' | 'spei' | 'swift' | 'faster_payments' | 'co_transfer' | 'uaefts' | 'eft' | 'cnaps' | 'zengin' | 'nip' | 'cce' | 'rtc': Canonical payment-rail vocabulary, shared by named accounts and payout destinations. The table below answers one question: can you open a named account on this rail (machine-readable in `x-ozav-rail-availability`)? Paying out is a different question, answered by the corridor table in `POST /payout-destinations`. The two differ: `fedwire`, for example, is live for payouts, while opening a USD account on it is sandbox only. | Rail | Corridor | Availability | |---|---|---| | `pix` | BRL - Brazil instant payments | available | | `fedwire` | USD - US wire | sandbox only | | `sepa` | EUR - SEPA credit transfer | live (behind enablement) | | `spei` | MXN - Mexico SPEI | sandbox only | | `swift` | Cross-border SWIFT | live (behind enablement) | | `faster_payments` | GBP - UK Faster Payments | live (behind enablement) | **`ach` and `co_transfer` are not offered.** Requests for accounts, payout destinations and quotes use `RequestRail`, which leaves both out, and a `fiat_payment` source on `ach` is refused with `422 RAIL_NOT_AVAILABLE`. They stay in this enum only because older records carry them. **`live (behind enablement)`** means the corridor is served in production, but not to everyone: opening an account still requires that OZAV has enabled the specific customer for foreign-currency named accounts. Until then a `live` request is refused with a named error that says which step is missing - not with `ACCOUNT_RAIL_NOT_AVAILABLE`, which would send you looking at the corridor instead of the customer. **`sandbox only` is a real, usable state, and not a promise of later.** The corridor is fully exercisable with a `test` key: you can open the account, read its receiving instructions and build against the exact response shape production will return. What it does not have is a settlement partner behind it, so a `live` key is refused with `ACCOUNT_RAIL_NOT_AVAILABLE` rather than opening something that cannot receive. Build against it in sandbox; the request you write does not change when the corridor goes live.
      - `virtual_account_id` string: Accepted and ignored: the payment is always received through the standard flow. Omit it.
      - `payment_instructions` PixPaymentInstructions: Pix payment instructions for a specific pay-in transaction.
        - `rail` 'pix', required: The payment rail; always `pix`.
        - `intent_id` string, required: OZAV's id for the Pix pay-in intent. **Opaque - store it and echo it back, never parse it.** Unlike the `ozav_*` resource ids, this one carries no prefix and is a UUID today; the format is not part of the contract and may change. Use it to correlate the Pix payment with this transfer, not to infer anything about the intent.
        - `br_code` string, nullable: Pix copy-and-paste BR Code for this specific transaction.
        - `qr_code_image` string, nullable: QR code image payload, when available.
        - `expires_at` string, date-time, nullable: When the Pix instructions expire.
    - TransferSourceWallet: A managed wallet as the source of funds (off-ramp / wallet transfer).
      - `type` 'wallet', required: Discriminator for a wallet source; always the literal `wallet`.
      - `wallet_id` string, required: The `ozav_wal_` id of the source wallet.
      - `currency` string: Uppercase ISO 4217 code or stablecoin symbol (e.g. BRL, USD, EUR, MXN, USDC, USDT). Uppercase is the canonical form: values are accepted case-insensitively on input but are always uppercase in responses.
      - `chain` string: The network the funds leave from, when the wallet serves more than one. Same rule as on a wallet destination: with the FX rate lock it is what the hedge follows, and an absent network refuses the lock instead of guessing.
    - TransferSourceCryptoPayment: An inbound crypto deposit as the source (crypto receive).
      - `type` 'crypto_payment', required: Discriminator for a crypto-payment source; always the literal `crypto_payment`.
      - `deposit_address_id` string, required: The id of the crypto deposit address the funds arrive at.
    - TransferSourceAccount: The customer's own named fiat account (`ozav_acc_`) as the source of an on-ramp: the customer already deposited into that account, and the order is funded from its balance. No payment instructions are returned, because there is nothing left to pay. The account must be active and known to its institution, and the amount must fit the balance that is not already committed to the customer's other open orders. In the sandbox, a test key can use a sandbox account after `POST /accounts/{account_id}/simulate/credit`. Choose `fiat_payment` instead when the customer has not deposited yet and needs payment instructions to send the money.
      - `type` 'account', required: Discriminator for an own-account source; always the literal `account`.
      - `account_id` string, required: The `ozav_acc_` id of the customer's own named fiat account.
  - `destination` union, required: Where a transfer's funds go. Discriminated by `type`; the value selects the flow together with the source.
    - TransferDestinationPayout: A registered payout destination as the target (FX / off-ramp destination leg).
      - `type` 'payout_destination', required: Discriminator for a payout-destination target; always the literal `payout_destination`.
      - `payout_destination_id` string, required: The `ozav_pd_` id of the registered payout destination.
    - TransferDestinationWallet: A managed wallet as the target (on-ramp / wallet transfer).
      - `type` 'wallet', required: Discriminator for a wallet target; always the literal `wallet`.
      - `wallet_id` string, required: The `ozav_wal_` id of the destination wallet.
      - `chain` string: The network the credit lands on, when the wallet serves more than one. A managed wallet serves several networks, so a BRL Pix on-ramp into one requires it: without it the transfer is refused with `DESTINATION_CHAIN_REQUIRED` before any charge is created. **Optional in general, but effectively required when you use the FX rate lock.** The hedge OZAV buys is withdrawn to treasury on the same network as the delivery - buying on one and delivering on another would leave a crossing for someone to resolve with the exposure already open. With no network supplied there is nothing to follow, so the lock is **refused** (`facility.reason = sem_destino_de_tesouraria`) rather than guessed. The transfer itself still proceeds; only the lock is lost. The refusal can also mean the pair itself has no registered treasury destination (`USDC:tron`, for instance, is not a pair OZAV serves). The message names the `asset:network` pair, so read it before assuming the field is at fault.
    - TransferDestinationCryptoAddress: External crypto destination; triggers wallet screening and travel-rule requirements by policy.
      - `type` 'crypto_address', required: Discriminator for an external crypto-address target; always the literal `crypto_address`.
      - `address` string, required: The external blockchain address to send to; must be valid on `network`.
      - `network` 'ethereum' | 'base' | 'optimism' | 'polygon' | 'avalanche_c_chain' | 'worldchain' | 'unichain' | 'linea' | 'sonic' | 'sei' | 'celo' | 'zksync_era' | 'solana' | 'tron' | 'stellar', required: Public v2 blockchain network vocabulary for wallet balances and crypto transfer legs. This enum is the wire contract today and lists only networks accepted by public v2 fields now. The custody stack can have additional active networks before those networks are accepted by every public API flow; see `x-ozav-network-matrix` for that explicit split.
      - `currency` string, required: Uppercase ISO 4217 code or stablecoin symbol (e.g. BRL, USD, EUR, MXN, USDC, USDT). Uppercase is the canonical form: values are accepted case-insensitively on input but are always uppercase in responses.
    - TransferDestinationAccount: The customer's own named fiat account (`ozav_acc_`) as the target of an off-ramp: the stablecoin is sold and the fiat is credited to that account, where it stays until the customer moves it. Available for USD, EUR and GBP accounts that are active and known to the account's institution; the quote's target currency must be the account's currency. Choose this when the customer wants to hold a fiat balance rather than pay a third party; choose `payout_destination` to pay out to a registered external account.
      - `type` 'account', required: Discriminator for an own-account target; always the literal `account`.
      - `account_id` string, required: The `ozav_acc_` id of the customer's own named fiat account.
  - `travel_rule` TravelRule: Originator and beneficiary information for a crypto send, required by BCB Resolution 520/2025 when value leaves custody to an external address. **Read the "Required for an external crypto send" list below before building this object.** The fields marked required *there* are not the same as the ones marked required *here*: this schema is shared with paths where Travel Rule is supplementary, so it stays permissive. The mandatory set is enforced on `destination.type = crypto_address`, and a request that misses any of it is refused with `422 TRAVEL_RULE_REQUIRED` and a `details.missing_fields` array naming each one. ### Required for an external crypto send | field | rule | |---|---| | `originator.name` | non-blank | | `originator.identity_documents[].type` | at least one entry must be accepted for the customer's country - see the table below | | `originator.identity_documents[].number` | non-blank; for `cpf` and `cnpj`, the check digits must be valid (`cnpj` accepts the alphanumeric format) - otherwise `missing_fields` names `originator.identity_documents.number(digito_verificador_invalido)` | | `originator.identity_documents[].issuing_country` | required on **every** entry | | `originator.address.street_line_1` | non-blank (`street` and `line1` are accepted aliases) | | `originator.address.city` | non-blank | | `originator.address.country` | non-blank | | `beneficiary.name` | non-blank | | `beneficiary.wallet_type` | `external` or `self_custodied` (`hosted` is rejected on this path) | | `beneficiary.wallet_attested_ownership_at` | ISO-8601 timestamp of when the holder attested ownership; must not be in the future | | `beneficiary.is_self` | boolean; `true` when the destination belongs to your own customer | #### Accepted originator identity documents, by country The requirement follows the customer's country of residence as held on the customer record - **not** the country you send inside `travel_rule`, which is stored but never selects the regime. A country not listed below uses the default set. To be precise about where that country comes from: you declare it when you create the customer (`individual.tax_id.country`, or the address), and you can correct it later with `PATCH /customers/{id}`. Both paths refuse a jurisdiction OZAV does not serve. So the regime is not selectable *per request*, but it does follow what you declared for that customer - keep it accurate. | country | accepted `identity_documents[].type` | |---|---| | `BR` | `cpf`, `cnpj` | | `NG` | `bvn`, `nin`, `passport`, `national_id` | | *(default)* | `passport`, `national_id` | If your customer's country is not listed and the default set does not fit its national identifiers, tell us and it gets modelled - the list is data, not code. A destination that is an OZAV wallet id (`ozav_wal_...`) is exempt: the value stays in custody and both sides are already verified. Values inside `originator` and `beneficiary` must be plain strings - do not embed internal or third-party references.
    - `originator` TravelRuleParty: One side of a Travel Rule record. `required` here lists only what every path needs; see `TravelRule` for the stricter set an external crypto send enforces.
      - `name` string, required: Full legal name of the party, as it appears on their verified identity.
      - `account` string: The wallet address or account reference for this party.
      - `identity_documents` object[]: Identity documents for this party. On an external crypto send the originator must carry at least one entry whose type is accepted for the customer's country (see "Accepted originator identity documents, by country"). Accepted under the legacy name `identifying_information` as well; both normalize to the same record.
        - `type` string, required: Document kind, lowercase. Which kinds satisfy the originator requirement depends on the customer's country - see "Accepted originator identity documents, by country" above. Other kinds are accepted and stored but do not satisfy it on their own. A refusal names the kinds that do, in `details.missing_fields` as `originator.identity_documents[<kind>|<kind>]` and in the message. Note that the customer resource types are a different set: `GET /customers` returns `type: other` for a document whose check digits it cannot validate, and `ssn`, `ein`, `nif` and `company_number` for others. None of those satisfy the originator requirement, so for a customer outside the table send `passport` or `national_id` here rather than copying the customer resource's type. A customer whose country is missing, or is not a 2-letter code, is read as `BR`, the strictest row.
        - `number` string, required: The document number. Non-blank. For `cpf` and `cnpj` on an external crypto send, the check digits are validated; `cnpj` accepts the Receita Federal alphanumeric format (12 alphanumeric + 2 numeric check digits). Punctuation is ignored.
        - `issuing_country` string, required: ISO 3166-1 alpha-2 country that issued the document.
      - `address` object: Postal address. Required for the originator on an external crypto send. `street_line_1` is the canonical key; `street` and `line1` are accepted aliases.
        - `street_line_1` string
        - `street_line_2` string
        - `city` string
        - `state` string
        - `postal_code` string
        - `country` string: ISO 3166-1 alpha-2.
      - `wallet_type` 'hosted' | 'external' | 'self_custodied': How the party's wallet is held. On an external crypto send the beneficiary must be `external` or `self_custodied`; `hosted` is rejected there.
      - `wallet_attested_ownership_at` string, date-time: When the holder attested ownership of the wallet.
      - `is_self` boolean: True when this party is your own customer (a send to their own external wallet) rather than a third party.
    - `beneficiary` TravelRuleParty: One side of a Travel Rule record. `required` here lists only what every path needs; see `TravelRule` for the stricter set an external crypto send enforces.
      - `name` string, required: Full legal name of the party, as it appears on their verified identity.
      - `account` string: The wallet address or account reference for this party.
      - `identity_documents` object[]: Identity documents for this party. On an external crypto send the originator must carry at least one entry whose type is accepted for the customer's country (see "Accepted originator identity documents, by country"). Accepted under the legacy name `identifying_information` as well; both normalize to the same record.
        - `type` string, required: Document kind, lowercase. Which kinds satisfy the originator requirement depends on the customer's country - see "Accepted originator identity documents, by country" above. Other kinds are accepted and stored but do not satisfy it on their own. A refusal names the kinds that do, in `details.missing_fields` as `originator.identity_documents[<kind>|<kind>]` and in the message. Note that the customer resource types are a different set: `GET /customers` returns `type: other` for a document whose check digits it cannot validate, and `ssn`, `ein`, `nif` and `company_number` for others. None of those satisfy the originator requirement, so for a customer outside the table send `passport` or `national_id` here rather than copying the customer resource's type. A customer whose country is missing, or is not a 2-letter code, is read as `BR`, the strictest row.
        - `number` string, required: The document number. Non-blank. For `cpf` and `cnpj` on an external crypto send, the check digits are validated; `cnpj` accepts the Receita Federal alphanumeric format (12 alphanumeric + 2 numeric check digits). Punctuation is ignored.
        - `issuing_country` string, required: ISO 3166-1 alpha-2 country that issued the document.
      - `address` object: Postal address. Required for the originator on an external crypto send. `street_line_1` is the canonical key; `street` and `line1` are accepted aliases.
        - `street_line_1` string
        - `street_line_2` string
        - `city` string
        - `state` string
        - `postal_code` string
        - `country` string: ISO 3166-1 alpha-2.
      - `wallet_type` 'hosted' | 'external' | 'self_custodied': How the party's wallet is held. On an external crypto send the beneficiary must be `external` or `self_custodied`; `hosted` is rejected there.
      - `wallet_attested_ownership_at` string, date-time: When the holder attested ownership of the wallet.
      - `is_self` boolean: True when this party is your own customer (a send to their own external wallet) rather than a third party.
  - `metadata` Metadata: Free-form string map (max 50 keys).
  - `facility_terms_accepted` boolean: **The FX rate lock (`trava de câmbio`), on this transfer.** Send `true`, together with `facility_terms_version`, to lock the rate at acceptance instead of converting at the price of the moment the money actually moves. Both fields are optional and omitting them is the normal path - a transfer without them behaves exactly as before and the response carries no `facility` block. **Only BRL/Pix flows honour the lock.** The fields validate on every combination, but on a `crypto_address` destination, a wallet-to-wallet move, or the BRS redemption corridor they are accepted and have no effect - no lock is taken and no `facility` block is returned. Always branch on the presence of `facility` in the response rather than assuming the request implied a lock. The lock is a contracted facility: your organization needs a signed schedule (`Termo`) and a funded reserve before it is available, and without them the request is refused with `422 FACILITY_LOCK_UNAVAILABLE`. **Strictly `true`, not truthy.** `"true"`, `1` and `{}` all read as "no lock requested", silently - the transfer is created unlocked. **Get the version from the preview first:** `POST /quotes` with `facility_preview: true` returns a `facility` block whose `terms_version` is the only value this route accepts. Accepting without a version is refused with `422 FACILITY_TERMS_VERSION_REQUIRED`, because an acceptance that does not say what was accepted cannot support a charge later. **Where the lock actually engages differs by direction, and the off-ramp case is not obvious.** On a BRL/Pix on-ramp it engages at acceptance. On a BRL/Pix off-ramp it engages only when the customer's custody balance is insufficient - with enough balance the withdrawal takes the ordinary path and the lock does not apply. That outcome is reported in the response's `facility` block rather than passed over in silence.
  - `facility_terms_version` string: The `terms_version` returned by the facility preview (`POST /quotes` with `facility_preview: true`). Required whenever `facility_terms_accepted` is `true`; rejected as stale when it does not match the schedule currently in force for your organization (`422 FACILITY_LOCK_UNAVAILABLE` with `details.reason = terms_version_mismatch` on the on-ramp, `409 PREFUNDING_TERMS_VERSION_STALE` on the off-ramp). Re-read the preview and accept again.

## Response `200`

Degraded success - the transfer was dispatched, but finalizing the record afterwards failed (projection persist, owner binding, or serialization). The payment may already be on its way, so nothing is rolled back: the transfer is downgraded to `needs_review` and this reduced body is what you get. It is not the full Transfer resource - `source`, `destination`, `amounts` and `quote_id` are absent, and a client that assumes the `201` shape here will read `undefined` on the one response that means "money may have moved".

Branch on the status code, not on `res.ok`. On a `200`, treat the transfer as unknown and reconcile with `GET /transfers/{id}` - do not retry the create, and do not present it to the payer as failed. A retry with the same `Idempotency-Key` replays this same body.

- object
  - `id` string, required: The transfer id - use it to reconcile.
  - `object` 'transfer', required
  - `status` 'needs_review', required

## Other responses

- `201`: Transfer created.
- `202`: Accepted - a concurrent request with the same `Idempotency-Key` is still executing; the body is `{"status": "processing"}`. Retry with the same key once the original completes: a completed request replays its stored response verbatim with the original status code and the `Idempotent-Replayed: true` header. This is not the resource - check the status code before deserializing. If this `202` repeats instead of resolving, the original request's outcome is unknown and no stored result will arrive - the key stays claimed when a request fails after the resource may already have been written. Stop retrying, reconcile with a `GET` to see whether the resource exists, and use a new key if it does not.
- `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.
- `409`: Conflict (e.g. IDEMPOTENCY_KEY_REUSED).
- `422`: Well-formed but not executable. Codes: NOT_AVAILABLE (the selected non-BRL/Pix wallet or crypto leg is not available, or a `via` quote sent from something other than a Pix payment, on a test key, or with `facility_terms_accepted`), WALLET_REQUIRED (a `via` quote needs the customer's OZAV wallet for the stablecoin), TARGET_CURRENCY_NOT_AVAILABLE, UNSUPPORTED_COMBINATION (the source/destination pair is not defined), RAIL_NOT_AVAILABLE (no settlement-ready coverage for the requested rail/corridor), QUOTE_EXPIRED (request a new quote), QUOTE_NETWORK_MISMATCH (the quote priced the network cost for a different `source.chain`; request a new quote with the right one), QUOTE_WITHOUT_NETWORK_COST (the quote did not price the network cost; request a new quote with `source.chain`), CUSTOMER_NOT_ENABLED (the payout destination is not yet enabled for transfers), PROVISIONING_REQUIRED (the customer or destination is not yet provisioned at the settlement layer), FACILITY_TERMS_VERSION_REQUIRED (the optional FX rate lock was accepted without `facility_terms_version`), and FACILITY_LOCK_UNAVAILABLE (the optional FX rate lock was refused by account state; nothing was created, and `details.reason` names the refusal - branch on it, not on the message).
- `429`: Too many requests. Read `code` first. `RATE_LIMIT_EXCEEDED` is the per-minute limit: wait the number of seconds in the `Retry-After` response header before retrying; retrying sooner just consumes budget without being served. A cap with its own code (for example `CREDENTIAL_ISSUANCE_LIMIT_REACHED`, which resets the next day) carries no `Retry-After`, and retrying sooner than its stated reset cannot succeed.

---

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