---
title: "Create an FX quote"
method: POST
path: "/quotes"
tags: ["Quotes"]
---

# Create an FX quote

`POST /quotes`

## Request

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

Quotes are opaque and indicative (`floating`: the rate re-derives at
execution), and carry the OZAV all-in client-facing rate. When enabled, they also carry the additional client-selected
spread in basis points. A quote is executed by creating a transfer that
references it.

**Setting your own spread (revenue share).** Pass the optional
`spread_bps` to add basis points on top of the OZAV all-in rate for
this quote. This value is your additional client-selected spread; it
rides the quote through to settlement and is echoed back as
`exchange_conditions.spread_bps`. OZAV cost, floor, margin, and split
analytics are internal and are never exposed in the public response.
Availability of client-selected spread is gated on enablement for your
organization.

**Sandbox.** With a test key (`ozav_sk_test_`), quotes are simulated.
Sandbox quotes use the current production reference for the currency
pair plus a 1 bp sandbox margin and your configured spread, the way
production prices them. When no current reference exists for a pair,
a fixed table stands in. Sandbox quotes are not executable.

## Request body

- object: Provide exactly one of source.amount or target.amount.
  - `customer_id` string, required: The opaque customer id (ozav_cus_...). Resolved scoped to the caller; an unknown or cross-tenant id returns 404.
  - `source` object, required
    - `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.
    - `amount` string: Decimal string at accounting precision - up to 6 fractional digits. Never a float. The value must be greater than zero, with no leading zeros in the integer part (a bare `0` integer part is allowed for sub-unit values such as "0.50") and at most 15 integer digits.
    - `chain` string: **Send this when the source is a stablecoin.** The network the funds will leave from (`tron`, `ethereum`, `polygon`, `solana`, and so on), the same value you send as `source.chain` on `POST /transfers`. The network cost of moving the stablecoin is part of the price, and it differs by orders of magnitude between networks, so a quote for a stablecoin source needs to know the network. A quote that cannot determine it is refused with `422 NETWORK_COST_UNPRICED`; a transfer whose `source.chain` differs from the one the quote priced is refused with `422 QUOTE_NETWORK_MISMATCH`. Ignored for fiat sources.
  - `target` object, required
    - `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.
    - `amount` string: Decimal string at accounting precision - up to 6 fractional digits. Never a float. The value must be greater than zero, with no leading zeros in the integer part (a bare `0` integer part is allowed for sub-unit values such as "0.50") and at most 15 integer digits.
    - `chain` string: **Send this when the target is a stablecoin.** The network the stablecoin will be delivered on (`tron`, `ethereum`, `polygon`, `solana`, and so on). Delivering a stablecoin is an on-chain transfer, and its network cost differs by orders of magnitude: the same delivery costs about US$ 3.39 per leg on `tron` and about US$ 0.001 on `solana` or `base`. Sending this lets the quote price that cost instead of guessing it. Your account's configured network is used when you omit it. Where neither is available the quote is still returned, without the network cost priced in - but the network you actually deliver on is the one that decides what the operation costs, so send it. Ignored for fiat targets.
  - `source_rail` 'pix' | 'fedwire' | 'sepa' | 'spei' | 'swift' | 'faster_payments' | 'uaefts' | 'eft' | 'cnaps' | 'zengin' | 'nip' | 'cce' | 'rtc': The rails a request can name: every value of `Rail` except `ach` and `co_transfer`, which are not offered and appear only on older records.
  - `destination_rail` 'pix' | 'fedwire' | 'sepa' | 'spei' | 'swift' | 'faster_payments' | 'uaefts' | 'eft' | 'cnaps' | 'zengin' | 'nip' | 'cce' | 'rtc': The rails a request can name: every value of `Rail` except `ach` and `co_transfer`, which are not offered and appear only on older records.
  - `destination_country` string: Optional. ISO 3166-1 alpha-2 country of the beneficiary's bank, for a fiat target. Some currencies cost more to deliver in one country than another (USD paid in China, say), and the quote prices the country you send. Without it, the quote uses the currency's default country. A currency priced only for other countries answers `422 CORRIDOR_NOT_PRICED_FOR_COUNTRY` (`param: destination_country`). It cannot be combined with `via` yet (`400 DESTINATION_COUNTRY_NOT_SUPPORTED_WITH_VIA`).
  - `spread_bps` integer: Optional. Additional client-selected spread for this quote, in integer basis points. **This is the spread you charge your end customer, not an OZAV charge; it is revenue-shared with OZAV under your commercial agreement.** Available only when enabled for your organization. A non-integer, negative, or out-of-range value is rejected with 400. **It is applied inside `rate`.** The quote you receive is already marked up by this many basis points, so comparing that rate to a market reference (an exchange spot price, say) will look worse by about this amount. It is echoed back as `exchange_conditions.spread_bps`, and `exchange_conditions.rate_without_spread` returns the rate before it. **Omitting it applies your configured default**, which can be different from `0` if one was negotiated for your organization. Send `0` explicitly to quote at the OZAV all-in rate (all-in except for any `amount_deducted`). **BRL to BRS has a tighter ceiling than the `maximum` above.** On that corridor the *total* charged spread cannot exceed **1000 bps**, and the OZAV margin for the corridor counts towards that total, so the largest value you can send is lower than 1000 by that margin. Above the ceiling the quote is refused with `422 BRS_SPREAD_ABOVE_CAP` (`param: spread_bps`, `details.max_total_spread_bps`), never silently trimmed to fit: a quote that froze a value the settlement would refuse would take payment for an operation that cannot complete. The same request on the quote and on the payment intent is refused the same way.
  - `funding_source` 'incoming_transfer' | 'named_account_balance': Optional. How the source fiat is funded. `incoming_transfer` includes the measured inbound cost (USD SWIFT US$ 35; EUR SEPA / GBP Faster Payments US$ 5, deducted from the credit). `named_account_balance` converts funds already on the named account and includes the outbound cost to the desk (USD wire US$ 35 + 12 bps; EUR/GBP local US$ 5 + 12 bps, charged on top). Omitted behaves as `incoming_transfer` for USD, EUR and GBP.
  - `facility_preview` boolean: Ask for the **FX rate lock conditions** alongside the quote. When `true` and your organization has the facility contracted, the response carries an extra `facility` block; `POST /transfers` then accepts its `terms_version` as `facility_terms_version`. This is the only place that value comes from. **Silent when unavailable.** A client without the facility gets a normal `201` quote with no `facility` block and no error - the preview never fails a quote, because the quote is the product. Read the absence of the block as "not available for this account", and use `POST /transfers` (which refuses loudly with `422 FACILITY_LOCK_UNAVAILABLE` and a `details.reason`) when you need to know why. **It prices, it does not reserve.** `capacity_guaranteed` is always `false`: room under the shared ceiling is resolved at acceptance, inside the lock. A preview that promised capacity would be a promise the design cannot honour.
  - `via` object: Optional. **Quote BRL into a foreign currency in a single quote**, passing through a stablecoin: OZAV buys the stablecoin with the BRL and pays the foreign currency out of it. The response carries the end-to-end `rate`, plus `via` and `legs`. Each leg is priced on its own, and the second leg is priced on exactly the stablecoin the first delivers. - The source must be `BRL` and the target a foreign payout currency (USD, EUR, GBP, ...). Send `source.amount`: `target.amount` is refused with `400 SOURCE_AMOUNT_REQUIRED`. - Live keys: enabled per organization, otherwise `422 VIA_NOT_ENABLED` (`param: via`). Once enabled, every payout currency in the `POST /payout-destinations` corridor table is open. Test keys quote it freely against the sandbox simulator; while the simulator is unavailable they get `422 RAIL_NOT_AVAILABLE` (`param: via`). - `spread_bps` applies once, on the first leg. The top-level `exchange_conditions` echoes it as `spread_bps` and carries no `rate_without_spread` (an end-to-end rate has no derivable "without spread"); each leg carries its own `exchange_conditions`. - The payout route caps each transfer: below its minimum you get `400 AMOUNT_BELOW_MINIMUM`, above its maximum `400 AMOUNT_ABOVE_MAXIMUM` (split the operation). - The network is `via.chain`. Not combinable with `source.chain`, `target.chain`, `facility_preview` or `funding_source` (`400 FIELD_NOT_ALLOWED`, `param` names the field). - Floating, like every quote: each leg re-prices at execution. Refusals specific to `via`: - `400 UNSUPPORTED_PAIR`: the source is not `BRL` (`param: source.currency`) or the target is not a foreign payout currency (`param: target.currency`). - `400 UNSUPPORTED_CURRENCY` (`param: via.currency`) and `400 SOURCE_CHAIN_UNSUPPORTED` (`param: via.chain`). - `422 RAIL_NOT_AVAILABLE`: no payout route covers the currency through that stablecoin right now (`param: target.currency`), or the requested `destination_rail` is not served by it (`param: destination_rail`). - `422 RAIL_NOT_AVAILABLE` (`param: via`): the network cost of the route cannot be priced right now. - `422 ASSET_NOT_AVAILABLE_ON_NETWORK` (`param: via.chain`). - `422 TICKET_BELOW_COST` (`param: source.amount`): the amount does not cover the route's costs, network cost included; increase `source.amount`. - `422 NO_QUOTE_AVAILABLE`: the two legs did not compose; request a new quote. **Executing it.** Send the quote to `POST /transfers` with `source` a Pix payment (`fiat_payment`, `BRL`, `rail: pix`) and `destination` a payout destination in the target currency. The response carries the Pix to pay. When the Pix settles, OZAV buys the stablecoin into the customer's OZAV wallet and pays the target currency out on its own; the transfer stays `converting` until the payout ends. - The payout leg re-prices when the Pix settles, so the amount delivered can differ from the quote's `target.amount`. - Everything the payout can refuse (destination, documents, minimum, maximum) is checked before the Pix is created, with the same codes as a direct payout. - The customer needs an OZAV wallet that holds the stablecoin: `422 WALLET_REQUIRED` (`param: customer_id`). - If the payout is refused after the Pix settles, the transfer moves to `needs_review` (event `transfer.updated`, not terminal) and the stablecoin stays in the customer's OZAV wallet until OZAV resolves it. - If the payout starts and then fails or is cancelled, the transfer ends `failed` or `cancelled` with `source_leg: completed` (the Pix was received). After `cancelled` the stablecoin is back in the customer's OZAV wallet; after `failed` it stays held until OZAV resolves it. - `target.amount` stays the quoted estimate; the transfer does not report the re-priced amount delivered. - Not available on test keys, nor with `facility_terms_accepted`: `422 NOT_AVAILABLE`. A `via` quote the API can no longer execute: `422 QUOTE_EXPIRED`, request a new one.
    - `currency` string, required: The stablecoin the conversion passes through (`USDT` or `USDC`).
    - `chain` string: Optional. The network the stablecoin moves on. Omit it and OZAV uses the network the payout route runs on. A network the route does not serve is refused with `422 ASSET_NOT_AVAILABLE_ON_NETWORK`.

## Response `201`

The quote.

- Quote: An FX quote - opaque, indicative (`floating`), and carrying the applied exchange conditions. Execute it by creating a transfer that references its `id`.
  - `id` string, required: Opaque quote identifier, prefixed `ozav_q_`.
  - `object` 'quote', required: Resource type discriminator; always the literal `quote`.
  - `customer_id` string: Opaque customer id (prefixed `ozav_cus_`) the quote was created for.
  - `rate_type` 'floating' | 'locked', required: How the `rate` behaves until execution. - `floating` - indicative; the rate re-derives at execution time. **Every quote returns this today**, and it comes with `expires_at: null`: a firm price holds for only a few seconds, so OZAV re-quotes at execution instead of expiring your quote. Treat `rate` as indicative: what executes can differ from the quote. - `locked` - held fixed until `expires_at`. **Not served on this API yet.** When it ships it will require the price-lock facility enabled on your account, and will carry a real `expires_at`. Ask OZAV if you need a firm price today. Do not branch on `expires_at` being non-null: on `floating` it is always `null`, in sandbox and in production alike.
  - `rate` string, required: The FX rate as a decimal string - target-currency units per one source unit, all-in except for any `amount_deducted` (it already carries any `spread_bps` you added). Indicative on a `floating` quote, which is every quote today: it re-derives at execution. On the quote, `source.amount x rate` gives `target.amount` (with `amount_deducted` present, `(source.amount - amount_deducted) x rate`).
  - `source` MonetaryAmount, required: An amount paired with its currency, at 6-fractional-digit accounting precision.
    - `amount` string, required: Decimal string at accounting precision - up to 6 fractional digits. Never a float. The value must be greater than zero, with no leading zeros in the integer part (a bare `0` integer part is allowed for sub-unit values such as "0.50") and at most 15 integer digits.
    - `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.
  - `target` MonetaryAmount, required: An amount paired with its currency, at 6-fractional-digit accounting precision.
    - `amount` string, required: Decimal string at accounting precision - up to 6 fractional digits. Never a float. The value must be greater than zero, with no leading zeros in the integer part (a bare `0` integer part is allowed for sub-unit values such as "0.50") and at most 15 integer digits.
    - `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.
  - `exchange_conditions` ExchangeConditions, required: Public exchange conditions. `spread_bps` is the additional client-selected spread for the quote, in integer basis points. OZAV cost, floor, margin, split and cost analytics are internal and are not serialized in public API responses.
    - `spread_bps` integer, required: Additional client-selected spread, in integer basis points (100 bps = 1%).
    - `rate_without_spread` string: The same quote before the `spread_bps` you selected, in the same scale as `rate`. When you did not select a spread it equals `rate` exactly. Use it to tell the spread you added apart from the OZAV rate. If you are comparing our quote to a market reference, compare `rate_without_spread`: `rate` already carries whatever spread you asked for. **To read your spread back exactly, measure both against the quote, not against each other.** Both rates are derived from the same internal base, so dividing one by the other returns slightly more than `spread_bps` (with a 10 bps floor, a 40 bps spread reads as 40.04). The value that is exact is `spread_bps` itself. Absent when the quote does not allow deriving it. OZAV cost, floor and margin stay inside both rates and are never serialized.
    - `amount_deducted` string: The amount taken off `source.amount` before conversion, in the source currency. Present only when an execution cost (the delivery network cost, the local payment rail cost and, on purchases, the settlement safety margin) is charged as an amount instead of being carried by the rate. Today that is limited to stablecoin purchases (fiat to USDT/USDC) on accounts configured for it, so its presence depends on the account, not on the corridor alone. The network cost depends on `target.chain`: send it. Sandbox does not emit it yet. On a transfer created from such a quote, `amounts.rate` is the all-in rate (`target.amount / source.amount`, 8 decimal places), so the transfer reconciles on its own, to that precision, without this field. With it, `(source.amount - amount_deducted) * rate` equals `target.amount`. That is the reconciliation to run when both are present. Absent, the plain `source.amount * rate` holds, as before. **It is published at the source currency's accounting precision, rounded UP.** The reconciliation therefore closes to within one minor unit of the source, and never resolves to more than `target.amount` promises. Rounding it down would let the arithmetic overstate what you receive. **It is not an OZAV charge, and it is not our cost.** It is what was deducted from this quote. OZAV cost, floor and margin stay internal and are never serialized.
  - `expires_at` string, date-time, nullable, required: When the quote expires (RFC 3339 / ISO 8601, UTC). **`null` on a `floating` quote is the normal answer, not a missing value, and it does not mean the quote is valid forever.** A floating rate is indicative: it re-derives at execution, so there is no window to "accept" it within. Quote as close to execution as you can, and read the rate you are given at execution as the one that applies. A timestamp appears only when the rate is held: a `locked` quote, which expires at this instant and is refused after it.
  - `created_at` string, date-time, required: When the quote was created (RFC 3339 / ISO 8601, UTC).
  - `facility` FacilityPreview: **The FX rate lock conditions (`trava de câmbio`), previewed alongside the quote.** Returned only when the request set `facility_preview: true` and your organization has the facility contracted and operable. Its `terms_version` is the value `POST /transfers` accepts as `facility_terms_version`, and this is the only place it comes from. **Absence is the answer, not an error.** A client without the facility gets a normal `201` quote with this block missing and no explanation - the preview never fails a quote, because the quote is the product. To learn why, attempt the transfer: it refuses loudly with `422 FACILITY_LOCK_UNAVAILABLE` and a machine-readable `details.reason`. **These numbers are contractual, not settings.** The penalty, the minimum charge, the reserve percentage, the client window and the cutoff come from the schedule (`Termo`) your organization signed. They are shown here because an acceptance over conditions the payer never saw does not support a charge later.
    - `terms_version` string: The version of the signed schedule these conditions come from. Echo it back as `facility_terms_version` on `POST /transfers`. It must match byte for byte.
    - `locked_rate` number: The rate implied by this quote, at this instant - indicative. The binding lock is priced at acceptance (`POST /transfers` with `facility_terms_accepted: true`) from a firm quote, and what the customer receives comes back as `facility.amount` on the `201`, net of your organization's exchange conditions. Do not present `locked_rate` as the final price. The rate that would be locked, at this instant, always expressed as **BRL per unit of the stablecoin** - the same base the acceptance records. On an off-ramp (stable to BRL) it is `expected_amount / source_amount`; on an on-ramp (BRL to stable) it is `source_amount / expected_amount`. Never a ratio in the other direction.
    - `source_amount` string: How much is being locked, in the source unit.
    - `source_asset` string: The source unit of `source_amount`.
    - `expected_amount_minor` string: What the customer receives at `locked_rate`, in the minor unit of `expected_amount_currency` - the quote's target currency: centavos (2 decimals) when the target is BRL, micro-units (6 decimals) when the target is a stablecoin such as USDC or USDT. A string, because the value can exceed what a JSON number holds exactly.
    - `expected_amount_currency` string: The currency of `expected_amount_minor`: the quote's target. `BRL` on an off-ramp preview, the stablecoin on an on-ramp preview. Read it before interpreting the amount - the minor unit follows it.
    - `client_deadline_at` string, date-time: By when the customer must deliver (pay the Pix, or hand over the asset). Missing it is what triggers the penalty below.
    - `settlement_deadline_at` string, date-time: OZAV's own settlement deadline.
    - `settlement_mode` 'T0' | 'T1': Settlement day for this lock.
    - `cutoff_time_brt` string: The daily cutoff, in BRT, from the schedule. **It and the client window cut each other short: the deadline is always whichever comes first.** With a 120-minute window and a 17:00 cutoff, locking at 16:00 gives 60 minutes, not 120, and locking at 16:30 gives 30. That is the schedule, not a defect - but it is the first thing that surprises an integrator, so branch on `client_deadline_at`, never on the window length.
    - `charge_rule` string: How the charge is composed if the customer does not deliver in time. Cumulative, and the penalty is the large part.
    - `penalty_pct` number: Penalty, as a fraction. `0.05` is 5%.
    - `minimum_charge_usd` number: The minimum charge per unsettled operation, in USD. **Not basis points** - an absolute amount.
    - `reserve_pct` number: The reserve (collateral) required, as a fraction of the open position - aggregate, including the new one, not per transaction. `0.02` is 2%. It must be constituted before acceptance and maintained while positions are open; if the ratio falls below the schedule's minimum, a top-up call is triggered.
    - `holiday_risk` boolean: `true` when the deadline crosses a business-day boundary the calculation cannot resolve against a holiday calendar. Treat the deadline as approximate and confirm.
    - `capacity_guaranteed` false: **Always `false`.** This preview prices the lock; it does not reserve room under the shared ceiling, which is resolved at acceptance. Reading headroom here and deciding later is exactly the non-atomic pre-check the pool exists to prevent - two clients would both see "there is room" and both proceed.
  - `facility_unavailable` FacilityUnavailable: **Why the rate-lock preview is not available for this account.** Returned instead of `facility` when the request set `facility_preview: true` and the facility cannot be offered. It never fails the quote - the quote is the product - it only explains the absence. **Read this instead of guessing at a missing `facility` block.** Branch on `reason`, which is the stable slug and the same vocabulary as `details.reason` on `422 FACILITY_LOCK_UNAVAILABLE`, so one mapping serves the preview and the refusal. **This preview runs the offer gates only** - the ones that decide whether a lock can be shown. It does not evaluate collateral or your contractual limits, because those are resolved at acceptance, inside the lock. So `collateral_insufficient` is **not** a value you will see here: it appears on `POST /transfers` instead. The table below is the complete set this endpoint can return. | `reason` | What it means | What to do | |---|---|---| | `client_not_exempt` | Your organization has not contracted the facility. | Talk to your account manager - no request will succeed until it is contracted. | | `no_client` | The key does not resolve to an organization the facility knows. | Contact support with the request id. | | `no_terms` / `terms_inactive` / `no_terms_version` / `invalid_terms` | The signed schedule (`Termo`) is missing, not active, or incomplete. | A contract step, not a parameter. Talk to your account manager. | | `past_client_cutoff` | Locking now is past today's cutoff. | Try earlier on the next business day. | | `client_window_too_short` | Today's cutoff is too close to leave a usable delivery window (under ten minutes). | Try earlier on the next business day. | | `client_window_exceeds_settlement` | Today's window does not fit settlement. | Ask for a shorter window, or try earlier. | | `unwind_not_ready` | The facility is not being offered right now. Not about your account. | Retry later; if it persists, contact support. | | `pool_config_missing` / `execution_margin_not_configured` / `settlement_mode_not_configured` | A platform setting the facility needs is missing. Not about your account. | Retry later; if it persists, contact support with the request id. | | `invalid_amounts` | The amounts on the quote cannot be used to price a lock. | Check `source.amount` and the target; if they look right, contact support. | | `previa_indisponivel` | The preview could not be computed. Not about your account, and not about your request. | The quote itself is valid and usable. If you need the preview, contact support with the `request_id` of this response. |
    - `reason` string, required: Stable, machine-readable slug. **Branch on this, never on `message`.**
    - `message` string, required: Human-readable explanation in English, safe to show an operator. It includes `(reason: <slug>)`. May change without notice.
  - `via` object: Present only on a quote requested with `via`: the stablecoin and the network the conversion passes through.
    - `currency` string, required
    - `chain` string, required
  - `legs` QuoteLeg[]: Present only on a quote requested with `via`. The two legs, in order: BRL → stablecoin, then stablecoin → target. The second leg is priced on the stablecoin the first delivers, rounded down to the cent; a remainder below 0.01 stays in your customer's wallet. The top-level `source` is the first leg's source and `target` the second leg's target.
    - `source` MonetaryAmount, required: An amount paired with its currency, at 6-fractional-digit accounting precision.
      - `amount` string, required: Decimal string at accounting precision - up to 6 fractional digits. Never a float. The value must be greater than zero, with no leading zeros in the integer part (a bare `0` integer part is allowed for sub-unit values such as "0.50") and at most 15 integer digits.
      - `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.
    - `target` MonetaryAmount, required: An amount paired with its currency, at 6-fractional-digit accounting precision.
      - `amount` string, required: Decimal string at accounting precision - up to 6 fractional digits. Never a float. The value must be greater than zero, with no leading zeros in the integer part (a bare `0` integer part is allowed for sub-unit values such as "0.50") and at most 15 integer digits.
      - `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.
    - `rate` string, required: Target units per one source unit, for this leg.
    - `exchange_conditions` ExchangeConditions, required: Public exchange conditions. `spread_bps` is the additional client-selected spread for the quote, in integer basis points. OZAV cost, floor, margin, split and cost analytics are internal and are not serialized in public API responses.
      - `spread_bps` integer, required: Additional client-selected spread, in integer basis points (100 bps = 1%).
      - `rate_without_spread` string: The same quote before the `spread_bps` you selected, in the same scale as `rate`. When you did not select a spread it equals `rate` exactly. Use it to tell the spread you added apart from the OZAV rate. If you are comparing our quote to a market reference, compare `rate_without_spread`: `rate` already carries whatever spread you asked for. **To read your spread back exactly, measure both against the quote, not against each other.** Both rates are derived from the same internal base, so dividing one by the other returns slightly more than `spread_bps` (with a 10 bps floor, a 40 bps spread reads as 40.04). The value that is exact is `spread_bps` itself. Absent when the quote does not allow deriving it. OZAV cost, floor and margin stay inside both rates and are never serialized.
      - `amount_deducted` string: The amount taken off `source.amount` before conversion, in the source currency. Present only when an execution cost (the delivery network cost, the local payment rail cost and, on purchases, the settlement safety margin) is charged as an amount instead of being carried by the rate. Today that is limited to stablecoin purchases (fiat to USDT/USDC) on accounts configured for it, so its presence depends on the account, not on the corridor alone. The network cost depends on `target.chain`: send it. Sandbox does not emit it yet. On a transfer created from such a quote, `amounts.rate` is the all-in rate (`target.amount / source.amount`, 8 decimal places), so the transfer reconciles on its own, to that precision, without this field. With it, `(source.amount - amount_deducted) * rate` equals `target.amount`. That is the reconciliation to run when both are present. Absent, the plain `source.amount * rate` holds, as before. **It is published at the source currency's accounting precision, rounded UP.** The reconciliation therefore closes to within one minor unit of the source, and never resolves to more than `target.amount` promises. Rounding it down would let the arithmetic overstate what you receive. **It is not an OZAV charge, and it is not our cost.** It is what was deducted from this quote. OZAV cost, floor and margin stay internal and are never serialized.

## 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.
- `422`: The request is well-formed but cannot be fulfilled - e.g. a `rail` that is not served (see `Rail.x-ozav-rail-availability`).
- `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.
- `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)
