# Quotes and orders — Swaps API

Buy and sell across the provider fan-out.

**Jump to an operation:**

- [POST /quotes](#price-a-buy-or-a-sell-swap-is-paused-available)
- [GET /quotes/{id}](#read-a-previously-created-quote-available)
- [GET /quote_estimates](#estimate-a-buy-or-sell-price-without-signing-in-dark-flag)
- [GET /quote_estimates/first_screen](#estimate-a-countrys-first-screen-buy-price-with-the-market-chosen-from-its-coverage-dark-flag)
- [GET /orders](#list-this-holders-buy-sell-and-swap-orders-available)
- [POST /orders](#create-an-order-from-a-quote-available)
- [GET /orders/{id}](#read-one-orders-state-and-money-truth-available)
- [POST /orders/{id}/cancel](#cancel-an-order-before-it-settles-available)
- [GET /orders/{id}/events](#list-one-orders-timeline-available)

> Generated from the bundled OpenAPI contract. The HTML page and this Markdown twin share the same processed operation descriptions and status annotations.

<a id="price-a-buy-or-a-sell-swap-is-paused-available"></a>
## `POST /quotes` — Price a buy or a sell (swap is paused) — available

**Operation ID:** `quotes.create`

> **Status:** Available · **Callers:** business key, agent (business key), dashboard session · **Scope:** `orders.write` · **Test mode:** unavailable

**Swap is paused (D-87):** `side: swap` answers `409 capability_unavailable` with `error.details.reason_code: TRADE_PAUSED` (`param: side`) before any pricing, until its release. `side: buy` and `side: sell` price. **Selling is live through Bridge (since 2026-10-15):** a sell executes through Bridge only, so a sell quote offers Bridge only: another provider's sell price is withheld and listed in `providers` as `paused` (`provider_paused`), never an offer (a sell no provider but Bridge would price answers `409 capability_unavailable`, `PROVIDER_PAUSED`). It takes a stablecoin on a launchable pair — USDC on arbitrum, avalanche, base, celo, ethereum, optimism, polygon, solana, stellar and tempo; USDT on ethereum, solana and tron; USDB on base — and pays out to a bank account in the seller's own name by ACH, wire, SEPA, Faster Payments, PIX or SPEI. Residents of the EEA sell USDC only: for a `country` in the EEA, USDT and USDB are not priced. On a Bridge sell `total_fees.swaps` is Swaps' 1 % fee, and on a sale that converts currency Bridge's FX spread is part of `total_fees.provider`. Runs the full provider fan-out for one route and returns the best executable offer, every fully priced provider / payment-method offer (`offers[]`), plus every provider's own terminal result (`providers[]`, D-21) from the existing call, without additional provider requests. This route requires a business key or bearer with orders.write; an anonymous caller gets an indicative, cache-only price from `GET /v1/quote_estimates` instead, which `POST /v1/orders` never accepts, and the legacy public widget transport is unchanged. Quotes expire; re-quote rather than reusing a stale one, and treat `quote_status: indicative` as provisional (`indicative_as_of`). For Transak, include the crypto network of a coin on several networks (`to_network` for a buy, `from_network` for a sell); an unspecified network cannot authorize that checkout. A buy of a coin on one network only (BTC, XRP, SOL, BNB…) is priced and checked out on that network when the request names none, and an explicit network always wins. A route with no executable offer is **409 `capability_unavailable`** carrying the dead-end context — never an empty 200 (RESOURCE-MODEL §2.4). To re-quote a specific method a caller (e.g. a payment-options chooser) just selected, call this again with that exact `payment_method` — the fan-out is pinned to it (BL-46); if the method has no route, the 409 names the reason in `message` and, structured, as `error.details.reason_code` (BS-MKT-1) whenever the dead end's reason code is one of the allowlisted reason codes this route names (`PAYMENT_METHOD_NOT_SUPPORTED`, `COUNTRY_NOT_SUPPORTED`, `LOCAL_CURRENCY_NOT_SUPPORTED`, `PROVIDER_UNAVAILABLE`, `PROVIDER_ERROR`, `PROVIDER_FILTER_MISMATCH`, `LIMIT_BELOW_MIN`, `LIMIT_ABOVE_MAX`, `QUOTE_NETWORK_REQUIRED`, `NETWORK_NOT_SUPPORTED`, `PROVIDER_PAUSED`, and `TRADE_PAUSED` for `side: swap` only), with `error.details.alternative_methods` when the server has one to suggest. **Amount outside every limit:** when every provider asked refused the amount on its own limit, the 409 names the amount — `param` is the amount field the request gave (`from_amount` or `to_amount`), `error.details.reason_code` is `LIMIT_BELOW_MIN` (every provider asked has a higher minimum) or `LIMIT_ABOVE_MAX` (a lower maximum) — and carries the bound those providers refused on as `Money` in the request leg it is denominated in: `error.details.minimum`, the smallest minimum, rounded up to that currency's scale, or `error.details.maximum`, the largest maximum, rounded down. It is the figure from which (or up to which) one of them prices; change the amount, not the route. If any provider asked failed or answered for another reason, the amount is not claimed and the answer is that reason (an outage stays `503 temporarily_unavailable`); the bound is omitted when the refusals name different currencies. `LIMIT_BELOW_MIN` from the route's own configured minimum, with no provider bound, names the amount without a figure. **Sell:** `from_network` is required and the asset and network must be a pair a sell can be made on (the set `POST /v1/orders` accepts) — without the network the answer is `409 capability_unavailable`, `param: from_network`, `error.details.reason_code: QUOTE_NETWORK_REQUIRED`; any other pair is the same 409 with `error.details.reason_code: NETWORK_NOT_SUPPORTED`; a payout currency the pair cannot be sold into is the same 409 with `param: to_asset` and `LOCAL_CURRENCY_NOT_SUPPORTED` (COP, which Bridge pays only by Bre-B, today), and a `payment_method` naming a payout rail the route cannot pay out on (`ach` for a EUR payout, say) the same 409 with `param: payment_method` and `PAYMENT_METHOD_NOT_SUPPORTED`; all before any pricing. A sell `from_amount` carries at most 8 decimals (fewer where the network's token scale is smaller: USDC and USDT 6), else `422 amount_invalid`, `param: from_amount` — the amount quoted is the amount ordered. **Paybis (BL-46, C4-D33):** every caller of this route holds `orders.write`, and `.claude/rules/money.md` freezes Paybis's own execution path pending review — `orders.create` has no reviewed way to execute it, so this route never returns Paybis as a selectable offer: `providers[]` always reports it `status: paused, reason: provider_paused`, `offers[]` never includes it, and the winning route is promoted to the next-best offer instead (or 409 `capability_unavailable` with `PROVIDER_PAUSED`, when Paybis was the only offer) rather than ever naming it the best route. The legacy public widget transport is unaffected. **Country (BSR-6, SEC-02):** `country`, when sent, must be an uppercase ISO 3166-1 alpha-2 code (`^[A-Z]{2}$`) — a different shape is `400 invalid_request`. A well-formed code this gateway does not support, or a comprehensively-sanctioned jurisdiction, is refused before the fan-out with `409 capability_unavailable` (`COUNTRY_NOT_SUPPORTED`), the same vocabulary `GET /v1/capabilities?product=buy_sell` already uses for `country`. This is judged even when `country` is omitted from the body: a caller-forwarded `x-vercel-ip-country` (or, on the direct Supabase-function host, the Cloudflare-set `cf-ipcountry`) is the same geoip fallback the fan-out itself would otherwise price with. Both forwarded headers are judged independently and unconditionally — a well-formed value on EITHER one naming a sanctioned jurisdiction is refused even when `country` names an unrelated, allowed one; neither header can mask the other, and an explicit `country` cannot mask either header. A header is held to a narrower rule than an explicit `country`: it is checked only against the sanctioned-jurisdiction list, not the full supported-country catalog, so a header that cannot name a real country (Cloudflare's `XX`/Tor's `T1`) or names a real, simply not-yet-catalogued one never causes a refusal on its own. **Amount scale (BSR-8):** for `side: 'buy'` only, `from_amount` is refused with `422 amount_invalid` when its fractional part carries more precision than `from_asset`'s own fiat scale — read from `packages/config/amountLimits.ts`'s `CURRENCY_PRECISION` (2 decimals for most currencies, 0 for the documented zero-decimal list, 3 for the Gulf dinars KWD/BHD/OMR/JOD/TND) — e.g. `from_amount: "20.999"` for `from_asset: "EUR"`. This is independent of the contract-layer shape bound (up to 18 fractional digits, wide enough for a full-precision crypto amount priced by destination): a value inside that shape can still be finer than its OWN currency can represent, and nothing downstream ever re-rounds it — it would otherwise be sealed verbatim into the deposit instructions on `POST /v1/orders`, an amount the payer has no way to actually send. A value padded with harmless trailing zeros beyond the scale (`"20.990"`) still quotes normally, as does a real 3-decimal amount for a Gulf dinar (`"1.001"` KWD). This check is scoped to `side: 'buy'` because that is the one direction whose `from_asset` is fiat (a sell's `from_amount` meets its own crypto-aware check instead — at most 8 decimals, fewer where the network's token scale is smaller, above — and `orders.create` checks the sealed amount again) — `sell`/`swap` never run THIS check, so an unlisted CRYPTO `from_asset` (e.g. an ERC-20 not in any decimals table) is never misjudged against a fiat-oriented fallback scale. `to_amount` is NOT held to this check either: pricing by destination amount routinely uses full on-chain precision for what the caller wants to receive, and nothing seals a raw `to_amount` into a payer-facing instruction the way `from_amount` is. **Rate (BSR-9):** `rate` — on the winning offer, every `offers[]` entry, every `providers[].offer` echo and `route_facts.best_price` — is derived server-side from each offer's `final_in`/`final_out` DECIMAL values, computed BEFORE either is rounded into the published `Money` fields, the same basis for every offer regardless of provider or side; because the derivation runs pre-rounding, cross-checking `rate` against the published `Money.amount` fields can drift in the last digit(s) for an asset whose published decimals are coarser than the server's own internal precision. It is informational only: never rank or compare offers by `rate`. Compare by `final_out` (higher is better) when the request supplied `from_amount` — every offer then targets a DIFFERENT `final_out` for the SAME pay amount. When the request supplied `to_amount` instead, compare by `final_in` (lower is better) ONLY among offers whose `final_out` equals the requested `to_amount` — an offer whose `final_out` differs is not comparable this way, because not every provider honors an exact-output target (a still-open gap: `_shared/adapters/coinbase.ts` prices a fixed default amount and ignores `to_amount` entirely, so a Coinbase offer's `final_out` in this mode can be unrelated to the request).

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `IdempotencyKey` | component | — | — | Reusable parameter component |

### Request body

Required: **yes**

| Media type | Schema |
| --- | --- |
| `application/json` | [QuoteRequest](/reference/~schemas.md#quoterequest) |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The best executable route, plus the full per-provider fan-out. | `application/json` [Quote](/reference/~schemas.md#quote) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `409` | No provider returned an executable offer for this route (`capability_unavailable`, carrying a safe reason code and terminal provider outcomes in `error.details.providers` when available, plus `error.details.reason_code` (BS-MKT-1) whenever the dead end's reason code is one of the allowlisted reason codes this route already names in `message` (`PAYMENT_METHOD_NOT_SUPPORTED`, `COUNTRY_NOT_SUPPORTED`, `LOCAL_CURRENCY_NOT_SUPPORTED`, `PROVIDER_UNAVAILABLE`, `PROVIDER_ERROR`, `PROVIDER_FILTER_MISMATCH`, `LIMIT_BELOW_MIN`, `LIMIT_ABOVE_MAX`, `QUOTE_NETWORK_REQUIRED`, `NETWORK_NOT_SUPPORTED` (a sell pair that cannot be sold), `PROVIDER_PAUSED`, `TRADE_PAUSED` (`side: swap` only, `param: side` — swap is paused)), and `error.details.alternative_methods` when a `PAYMENT_METHOD_NOT_SUPPORTED` re-quote has other methods to suggest). `LIMIT_BELOW_MIN` / `LIMIT_ABOVE_MAX` name the amount field in `param` and, when every provider asked refused the amount on its own limit, carry that bound as `Money`: `error.details.minimum` (the smallest minimum, rounded up) or `error.details.maximum` (the largest maximum, rounded down). Raw provider errors are never exposed. No empty 200 is returned. Fixer round 1 (2026-09-22) — also carries the SAME `capability_unavailable` `code` when the winning offer's own asset has no verified decimal scale (`error.details.asset` names it): this gateway's own gap, never the provider's, and NOT retryable — do not retry the identical request. | `application/json` [Error](/reference/~schemas.md#error) |
| `413` | — | `PayloadTooLarge` |
| `422` | `amount_invalid` (BSR-8): a `side: 'buy'` `from_amount` carries more decimal precision than `from_asset`'s own scale, or a `side: 'sell'` `from_amount` more than 8 decimals or the network's own token scale when that is smaller (USDC and USDT: 6) (`error.param: "from_amount"`) — refused before the provider fan-out ever prices it. See `from_amount`'s own schema description for the full rule and its scope. | `application/json` [Error](/reference/~schemas.md#error) |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | Test-mode quotes are not yet supported, or current upstream quote facts are unavailable — retryable. An unverified asset decimal scale on the winning offer is NOT this: see `409` above. | `application/json` [Error](/reference/~schemas.md#error) |

---

<a id="read-a-previously-created-quote-available"></a>
## `GET /quotes/{id}` — Read a previously created quote — available

**Operation ID:** `quotes.get`

> **Status:** Available · **Callers:** business key, agent (business key), dashboard session · **Scope:** `orders.read` · **Test mode:** unavailable

Re-reads a quote by id rather than re-running the fan-out (BL-46) — the exact `Quote` `quotes.create` already returned, replayed verbatim from the account-scoped snapshot it took at create time, never re-priced. `list` is still proposed. `404` covers both an unknown id and one that belongs to a different account — a caller never learns which, RESOURCE-MODEL §0.7. This is the read leg the Review screen's re-read-before-convert relies on (C4-D33): it lets a client fetch the exact quote it is about to act on instead of trusting a value it cached client-side.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | `string` | — |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The quote as it was created. | `application/json` [Quote](/reference/~schemas.md#quote) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="estimate-a-buy-or-sell-price-without-signing-in-dark-flag"></a>
## `GET /quote_estimates` — Estimate a buy or sell price without signing in — dark-flag

**Operation ID:** `quote_estimates.get`

> **Status:** Dark-flag — live behind a cohort or flag · **Callers:** public token · **Test mode:** unavailable
>
> This operation is **dark-flag — live behind a cohort or flag**. While its resource switch (`api_v1.<resource>`) is off, calling it answers `503 temporarily_unavailable`; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers `409 capability_unavailable`. Check its product page before relying on it.

An anonymous, indicative price for a buy or a sell — what the exchange widget shows while the person types, before they sign in (D-02; sell: A1 §2.3). PUBLIC and UNAUTHENTICATED: no key or session is read, and none is needed. It is NOT a quote: the answer (`QuoteEstimate`) has no `id` and no `offer_id`, and `POST /v1/orders` never accepts it. The binding price is `POST /v1/quotes` under a credential, at the review step. **Source:** the same quote cache the public widget reads — the in-memory cache, then the warm route matrix for a request with no `payment_method`, then one coalesced live provider fan-out on a miss — so a repeat of the same market never reaches the providers. Every answer is `quote_status: indicative` with the time the providers actually priced it (`indicative_as_of`) and an `expires_at` after which it is too old to show. **Scope:** `side=buy` and `side=sell`; `swap` answers `409 capability_unavailable` (`param: side`). Each offer carries the provider, the payment method when known, what is paid and received, the fee lines, the delivery time when the provider gave one, and `identity` — whether the route needs an ID document, from the same identity policy the authenticated quote path applies (any uncertainty reads `required: true`; a sell always does). **Sell:** `from_asset` is the stablecoin sent and `from_network` — required — the network it is sent on; `to_asset` is the fiat paid out; `to_network` is refused (`400`). Exactly one of `from_amount` (what is sent) or `to_amount` (what is paid out, Bridge's reverse quote). Only a launched sell pair is estimated — a Bridge offramp route Swaps has proven, on a network Bridge takes a deposit from, with an address format Swaps reads; `GET /v1/capabilities/buy_sell?side=sell` lists them per payout method (`sell_coverage[].assets`). Any other pair is `409 capability_unavailable` (`param: from_network`, `details.asset`, `details.network`) before any pricing, and so is a payout currency the pair cannot be paid out in on a rail a sell executes — Bre-B (COP) included, which Bridge prices but a sell order refuses (`param: to_asset`, `details.fiat`). Only Bridge sells today, and a sell estimate asks Bridge alone; another provider's sell price that still reaches it (a cached answer from before) is withheld and listed `paused`, never offered. On a sell `from_amount` is in the asset, `to_amount` and every fee line in the payout fiat — the same currency the binding sell quote states them in. Amount limits are not part of an estimate; read each method's limits from `GET /v1/capabilities/buy_sell` (`defaults.resolved_limits`). `providers[]` lists only the providers that priced the route, plus Paybis as `paused` — no availability claim for any other provider. Paybis is never offered, exactly as on `POST /v1/quotes`: it appears in `providers[]` as `paused` when it priced the route, and when it was the only price the answer is `409 capability_unavailable` (`PROVIDER_PAUSED`). **Country:** `country` is required, uppercase ISO 3166-1 alpha-2; a sanctioned or unsupported one is `409 capability_unavailable` (`COUNTRY_NOT_SUPPORTED`) before any pricing, and the forwarded geo headers are judged against the sanctioned list independently, exactly as on `POST /v1/quotes`. **Region:** for a US buy, `region` is the person's state (ISO 3166-2:US without the `US-` prefix, e.g. `CA`); a provider that needs the state — Stripe Onramp — prices the route only when it is given, and New York's own destination limits apply to `NY`. Without it the estimate is exactly what it was, and Stripe Onramp is absent from `offers` and `providers`. Outside the US, and on a sell, it is ignored. **Network:** on a buy, set `to_network` for an asset on more than one network; when the providers cannot price without it the answer is `409 capability_unavailable` with `QUOTE_NETWORK_REQUIRED`. An asset on one network only (BTC, XRP, SOL…) is priced on that network without one. **Amount:** exactly one of `from_amount` / `to_amount`; on a buy, a `from_amount` finer than the fiat's own scale is `422 amount_invalid`, and on a sell one finer than a sale takes (8 decimal places, capped by the asset's scale on its network: 6 for USDC and USDT) is `422 amount_invalid` too, before any pricing, exactly as `POST /v1/quotes` refuses it. An amount every provider asked refused on its own limit is `409 capability_unavailable` with `param` the amount field and `LIMIT_BELOW_MIN` + `error.details.minimum` or `LIMIT_ABOVE_MAX` + `error.details.maximum` (`Money`), exactly as on `POST /v1/quotes` — never `503`: retrying the same amount does not help, another amount does. **Protection:** a persistent per-client-IP limit (`429 rate_limit_error`; a limiter outage is `503`, never an unmetered answer), and a client IP on the Swaps block list is refused `403 ip_not_allowed` before any work. **Switch:** `dark-flag` — the `api_v1.quote_estimates` kill switch answers `503 temporarily_unavailable` while it is off, which it is until the founder turns it on at launch. **No MCP tool:** API-CANON §13 keeps quotes off the public agent surface; an agent prices through the authenticated `get_quote`. No test mode: there is no credential to carry one.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `side` | query | yes | `buy` \| `sell` \| `swap` | Direction: `buy` (fiat → asset) or `sell` (asset → fiat). `swap` answers `409 capability_unavailable`. |
| `from_asset` | query | yes | `string` | What the person gives. On a buy, the fiat they pay with — an uppercase ISO 4217 code (`EUR`). On a sell, the asset they send, by symbol (`USDC`). |
| `to_asset` | query | yes | `string` | What the person receives. On a buy, the crypto asset, by symbol (e.g. `USDC`, `USDC.e`). On a sell, the fiat paid out — an uppercase ISO 4217 code (`EUR`). |
| `from_network` | query | no | `string` | Sell only, and required there: the network the asset is sent on, as a lowercase slug (e.g. `base`, `tron`). With `from_asset` it names the sell pair; a pair Swaps has not launched is `409 capability_unavailable` (`param: from_network`). Read only on a sell. |
| `to_network` | query | no | `string` | Buy only: the network the asset is received on, as a lowercase slug (e.g. `base`, `ethereum`). Required in practice for an asset on more than one network — see `QUOTE_NETWORK_REQUIRED` above; an asset on one network only (`bitcoin` for BTC) needs none. Refused on a sell (`400`, `param: to_network`): the asset is sent there, on `from_network`. |
| `from_amount` | query | no | `string` | Amount to give (the fiat paid on a buy, the asset sent on a sell), a decimal string in `from_asset`'s major units, strictly positive. Exactly one of `from_amount` / `to_amount`. |
| `to_amount` | query | no | `string` | Amount to receive (the asset on a buy, the fiat paid out on a sell), a decimal string in `to_asset`'s major units, strictly positive. Exactly one of `from_amount` / `to_amount`. |
| `country` | query | yes | `string` | The country the person picked, uppercase ISO 3166-1 alpha-2. It narrows the providers and methods; it is not proof of residence. |
| `payment_method` | query | no | `string` | Price one payment method only. Omit to estimate every method the route offers. |
| `region` | query | no | `string` | The person's sub-national region, an uppercase ISO 3166-2 subdivision code without the country prefix (e.g. `CA`, `NY`). Read only for `country=US`, where it must be a US state, DC or outlying-area code (otherwise `400`, `param: region`) and lets Stripe Onramp price a buy; ignored for every other country and on a sell. A hint for an indicative price, not proof of residence — the binding quote and the provider's own checks decide. |
| `hosted_handoff` | query | no | `true` \| `false` | D-123 — `true` when the caller will order through `POST /v1/orders` and follow `next_action` to a provider-hosted payment page, a Bridge-native order included: only then is a Bridge buy by COP bank transfer — PSE (`co_bank_transfer`) — estimated. Absent or `false`: no such offer is estimated; every other offer is unchanged. The same value as `QuoteRequest.hosted_handoff`. Any other value is `400`, `param: hosted_handoff`. |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The estimate. Sent with `Cache-Control: no-store` — request a new one rather than replaying a stored copy. | `application/json` [QuoteEstimate](/reference/~schemas.md#quoteestimate) |
| `400` | — | `BadRequest` |
| `403` | — | `Forbidden` |
| `409` | `capability_unavailable` — `side` is `swap` (`param: side`); a sell pair Swaps has not launched (`param: from_network`, `details.asset`, `details.network`) or a payout currency it cannot be paid out in (`param: to_asset`, `details.fiat`); the country is sanctioned or unsupported (`COUNTRY_NOT_SUPPORTED`); or no provider priced the route, with the reason in `error.details.reason_code` when it is one `POST /v1/quotes` also names (`QUOTE_NETWORK_REQUIRED`, `PAYMENT_METHOD_NOT_SUPPORTED`, `PROVIDER_PAUSED`, …) and, when the providers answered live, their outcomes in `error.details.providers`; an amount outside every provider's limit is `LIMIT_BELOW_MIN` / `LIMIT_ABOVE_MAX` with `param` the amount field and the bound in `error.details.minimum` / `error.details.maximum` (`Money`). Do not retry the identical request. | `application/json` [Error](/reference/~schemas.md#error) |
| `422` | `amount_invalid` — on a buy, `from_amount` carries more decimal precision than `from_asset` supports (`param: from_amount`). | `application/json` [Error](/reference/~schemas.md#error) |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="estimate-a-countrys-first-screen-buy-price-with-the-market-chosen-from-its-coverage-dark-flag"></a>
## `GET /quote_estimates/first_screen` — Estimate a country's first-screen buy price, with the market chosen from its coverage — dark-flag

**Operation ID:** `quote_estimates.first_screen.get`

> **Status:** Dark-flag — live behind a cohort or flag · **Callers:** public token · **Test mode:** unavailable
>
> This operation is **dark-flag — live behind a cohort or flag**. While its resource switch (`api_v1.<resource>`) is off, calling it answers `503 temporarily_unavailable`; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers `409 capability_unavailable`. Check its product page before relying on it.

The same anonymous, indicative buy estimate as `GET /v1/quote_estimates` (`QuoteEstimate`, never a `Quote`), for the market the buy widget's first screen shows in a country — chosen by the server instead of named by the caller: the country's coverage defaults (`GET /v1/capabilities/buy_sell?country=…` `defaults` — the same projection, so a provider switched off is off here too) give the default fiat, the default coin on its first network and the suggested amount within that fiat's published limits (`defaults.resolved_limits`). That is the market a caller settles on once coverage has answered, so asking this while coverage is still on its way never prices a guessed currency: in a country whose default is not its own currency (Singapore → USD), an estimate in the local currency would be refused or thrown away. The answer names the market it priced in `first_screen` (`from_asset`, `to_asset`, `to_network`, `from_amount`). It is usually a cache read: the warm route matrix keeps exactly these markets fresh. Everything else is `GET /v1/quote_estimates`: the same checks, the same answer, the same `Cache-Control: no-store`. **Parameters:** `country` (required), `region` (a US buy's state) and `hosted_handoff`, as on `GET /v1/quote_estimates`; any market parameter (`side`, `from_asset`, `to_asset`, `from_network`, `to_network`, `from_amount`, `to_amount`, `payment_method`) is `400` with that `param` — the market is chosen here. **No market:** a country whose coverage lists no way to pay is `409 capability_unavailable` (`PROVIDER_UNAVAILABLE`, `param: country`); a coverage check that failed is `503 temporarily_unavailable`, never a guess. **Protection and switch:** the same per-client-IP limit, block list and `api_v1.quote_estimates` kill switch as `GET /v1/quote_estimates`. **No MCP tool** (API-CANON §13).

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `country` | query | yes | `string` | The country the person picked, uppercase ISO 3166-1 alpha-2. Its coverage defaults choose the market; it is not proof of residence. |
| `region` | query | no | `string` | As on `GET /v1/quote_estimates`: the person's state for `country=US` (ISO 3166-2:US without the prefix), which lets Stripe Onramp price; ignored for every other country. |
| `hosted_handoff` | query | no | `true` \| `false` | As on `GET /v1/quote_estimates` (D-123). Any other value than `true` or `false` is `400`. |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The estimate, with `first_screen` naming the market priced. Sent with `Cache-Control: no-store`. | `application/json` [QuoteEstimate](/reference/~schemas.md#quoteestimate) |
| `400` | — | `BadRequest` |
| `403` | — | `Forbidden` |
| `409` | `capability_unavailable` — the country is sanctioned or unsupported (`COUNTRY_NOT_SUPPORTED`), its coverage lists no way to pay (`PROVIDER_UNAVAILABLE`, `param: country`), or no provider priced the chosen market, with the reason in `error.details.reason_code` exactly as on `GET /v1/quote_estimates`. Do not retry the identical request. | `application/json` [Error](/reference/~schemas.md#error) |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="list-this-holders-buy-sell-and-swap-orders-available"></a>
## `GET /orders` — List this holder's buy, sell and swap orders — available

**Operation ID:** `orders.list`

> **Status:** Available · **Callers:** business key, agent (business key), dashboard session · **Scope:** `orders.read` · **Test mode:** unavailable

`transactions.list` behind a router (RESOURCE-MODEL §6). `status_group` is this resource's own bucket set (`needs_you`, `in_progress`, `done`), shared with payouts and payroll runs only — not a single cross-product enum.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `Limit` | component | — | — | Reusable parameter component |
| `Cursor` | component | — | — | Reusable parameter component |
| `status_group` | query | no | `needs_you` \| `in_progress` \| `done` | — |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | A page of orders, newest first. | `application/json` [OrderList](/reference/~schemas.md#orderlist) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="create-an-order-from-a-quote-available"></a>
## `POST /orders` — Create an order from a quote — available

**Operation ID:** `orders.create`

> **Status:** Available · **Callers:** business key, agent (business key), dashboard session · **Scope:** `orders.write` · **Test mode:** unavailable · **Money boundary:** per-transaction human confirmation required

Collapses `redirect_intent_create` → `redirect_init` → `redirect_execute` into one idempotent create for an authenticated caller (D-20, BS-G1, BL-47/C4-D33). A Bridge-native order (`checkout_readiness.handoff_mode: backend_native`) completes in place, landing directly on an active `status`, for `side: buy` and `side: sell` (L1-5; selling is live through Bridge since 2026-10-15). While swap is paused (D-87), a stored swap quote answers `409 capability_unavailable` with `error.details.reason_code: TRADE_PAUSED` before any provider is contacted; a `swap` offer is never created through this endpoint. For a buy, the selected offer's `execution_context.to_network` must also be set — `to_network` is optional on `POST /v1/quotes`, but a quote priced without it cannot become a Bridge-native order (`409 capability_unavailable`; request a new quote with `to_network` set).

**Hosted provider (L1-6c, widget v2 R5):** every other `handoff_mode` routes to a hosted-provider checkout — Transak, OnMeta, Stripe Onramp and Partna (Nigerian naira by bank transfer; `checkout_readiness.handoff_mode: signed_url`/`hosted_session`) — and BUY only (a hosted `side: sell` answers `409 capability_unavailable`; a hosted-sell deposit flow does not exist yet). A Partna buy executes only while `feature_flags.partna_quote_breakdown` is on. It ships off, so until it is turned on a Partna offer is refused `provider_not_supported` before any order is written (a pinned Partna `offer_id` answers `409 capability_unavailable`, `code: provider_not_supported`; without `offer_id` the order-time walk, L1-6e, skips it); while it is off, a Partna offer on a quote is priced by Partna's rate less the fees Partna states. Either way, a quote that gives `to_amount` has no Partna offer: Partna is priced only for an amount to pay. Everything below about Partna applies while it is on. `wallet_address` (or `address_book_id`) is required (`400 invalid_request` otherwise). An OnMeta, Stripe Onramp or Partna checkout is minted from this quote's own stored offer, never from the request: its `final_in` (or `execution_context.from_amount` when `final_in` is absent) is what the payer is charged, its `final_out` what they receive, and its `execution_context.to_network` the chain — an offer without `to_network` is `409 capability_unavailable`. The offer must still be fresh under its provider's checkout-readiness policy (handoff mode, safety window, maximum quote lifetime counted from the quote's `created_at`), otherwise `409 requote_required`. OnMeta and Partna re-price when their checkout is opened, and a Stripe Onramp checkout opened after its fresh window re-prices too: a re-price that moves the price by 0.5% (50 bps) or more is refused with `409 conflict`, `code: requote_required`, `error.details.reason: price_moved`, `error.details.drift_bps` and `error.details.quote_id` — no checkout URL is returned; request a new quote. That refusal comes after the order row was written, so it is not side-effect-free: that order is closed at once as `status: abandoned` with `money_state: never_authorized`, `money_status: not_charged`, `failure_kind: generic_failed` and a `failure_reason` (no checkout ever reached the payer), and is named in `error.details.order_id`. An OnMeta or Partna checkout that the provider cannot prepare is `503 temporarily_unavailable` with `error.details.quote_id` and the same abandoned `error.details.order_id`; request a new quote. Every checkout URL returned in `next_action` is `https` on a known provider host. `return_url` (optional) is where Transak and OnMeta send the payer back; Stripe Onramp's and Partna's hosted checkouts do not redirect back — the payer stays on the provider's page, and `GET /v1/orders/{id}` follows the order (its status comes from the provider's own notifications). The account resolved for this caller must have a verified email on file (`409 account_email_unavailable` otherwise — `email` on the request body is never substituted). A `201` carries `next_action {type: redirect, checkout_url, expires_at}` (`OrderSchema.checkout_stage` was dropped as a producer-less field, BL-38 #3086 — that hand-off window is expressed through `next_action`'s PRESENCE, not a separate enum) with `status: awaiting_payment`. A response replayed under the SAME `Idempotency-Key` is the stored body verbatim, so a replayed `next_action.expires_at` may already be in the past — check that field (or re-read `GET /v1/orders/{id}`, which resolves it fresh), do not assume a replay is fresh. **Re-create under a NEW `Idempotency-Key` (L1-6d):** a SEQUENTIAL second `POST /v1/orders` — one that starts after an earlier create for the SAME `quote_id` + `offer_id` has already returned or failed — never mints a second provider session for the active order that first create left behind. With a still-open checkout hand-off it returns `201` for that SAME order, its CURRENT `next_action`, and `Idempotent-Replayed: true`; with an expired/absent hand-off it answers `409 conflict` (`code: checkout_expired`, `error.details.order_id`, `sideEffectFree: true`) — request a new quote and create a new order, there is no way to re-mint a checkout session for an order that already has one. Fixer round 1 (#3/#8) — two creates for the same `quote_id` + `offer_id` genuinely CONCURRENT with each other (in flight at once, under different `Idempotency-Key`s) are not serialized by this check and can each mint their own provider session; reconcile any duplicate via `GET /v1/orders`. The rolling 24-hour value cap (below) applies in the source fiat; a currency with no explicit ceiling row is refused (`409 capability_unavailable`) rather than falling back to a Bridge-sized default. OnMeta's currencies have explicit rows: INR 1,500,000, IDR 45,000,000 and PHP 150,000 per account per rolling 24 hours, and Partna's NGN 15,000,000 (`422 amount_invalid` past them). An order this endpoint closed as `abandoned` with `money_state: never_authorized` (a refused re-price) does not count toward the cap. On a `503` from this branch with no `sideEffectFree` marker, a checkout session or a `pending` order row may already exist — reconcile via `GET /v1/orders` (`error.details.quote_id`), do not blindly retry under a new Idempotency-Key.

**Provider switches (widget v2 D-08):** an offer executes only while its provider is switched on in Swaps' provider configuration. A provider switched off, or with no switch recorded at all, is refused (`409 capability_unavailable`, `code: provider_paused`, `error.details.eligible_offer_ids`) or skipped by the no-`offer_id` walk; failing to read the switches is `503 temporarily_unavailable`, side-effect-free.

**Address book (widget v2 R7):** a dashboard session may send `address_book_id` instead of `wallet_address` for a buy; the entry's own address and network are used and its `last_used_at` is set after a `201`. A business key sending it is refused (`403 scope_denied`). See the field for the refusals.

Destination screening is fail-closed for a buy (both branches): a caller-supplied crypto `wallet_address` is refused (`403`) on a deny-list/velocity or sanctions hit, and on an unavailable screen under the default fail-closed policy (`503`). Paybis is refused (`409 capability_unavailable`, `provider_paused`) — `.claude/rules/money.md` still freezes `handleRedirectExecute`'s Paybis branch, and this route never reaches it (BL-46 already excludes Paybis from every `/v1/quotes` response; this is defense in depth for a quote created before that shipped). The offer's sealed source amount is also re-checked against its own decimal scale (`422 amount_invalid`, BSR-8) — for `side: 'buy'`, `POST /v1/quotes` already refuses a sub-scale `from_amount` before it can be priced, and this is defense in depth for a quote row minted before that shipped; for `side: 'sell'` (L1-5) the sealed crypto amount is held to at most 8 decimals, fewer where the network's token scale is smaller (USDC and USDT: 6) — the same rule `POST /v1/quotes` already applies to a sell `from_amount` before pricing, so this is defense in depth there too, for a sell quote minted before that check shipped. The public-token payer redirect stays a separate, browser-shaped surface and is unaffected by this operation.

**A Swaps Wallet as the destination (wallet phase 0 item 0.7):** a buy into a Swaps Wallet address is admitted only as every inbound door admits one — on Tempo, and only with a key Swaps can confirm — before any order row and any provider call: `422 destination_rail_keyless` (another network, or a hosted offer with no network bound), `422 destination_wallet_disabled`, `422 destination_wallet_key_reported_lost`, `422 destination_wallet_signer_at_risk` or `422 destination_wallet_signer_unconfirmed` (`param: wallet_address`; the address must change — do not retry), and `503 destination_risk_unverifiable` when the check could not run. Any other address is unaffected. These codes name a wallet's state, so they go only to the wallet's own holder (the account's owner buying into their own wallet): a buy into someone else's Swaps Wallet answers `422 destination_not_accepted` («This address can't take this transfer. Use another address.», `param: wallet_address`) for any of them, or `503 destination_check_unavailable` («We couldn't check this address right now. Try again shortly.») when the wallet or key-mark lookup itself failed — never the reason.

**Bridge-native sell (L1-5):** `external_account_id` is required — the saved bank destination from `GET /v1/wallet/external_accounts`, which must belong to this account's own Bridge customer and match the quote's `to_asset` and the offer's payout rail; a foreign or nonexistent id is `404` (never `403` — RESOURCE-MODEL §0.7), a currency/rail mismatch is `409 capability_unavailable`. The payout goes to a bank account in the seller's own name. The selected offer's `execution_context. from_network` must be set (`409 capability_unavailable` otherwise; request a new quote with `from_network` set). `wallet_address` is refused (`400 invalid_request`) — Bridge matches the incoming deposit from any sending wallet, so a declared source address would be a promise the server cannot keep. The `201` carries `deposit_instructions` — the crypto address to send from any wallet you control: send exactly `amount` of `asset` on `chain` to `address`, in one transfer, with `memo` when it is present (Stellar, Tron). `amount` is the amount the order was quoted for. The order sells the first deposit that arrives, whatever its amount (2026-10-28), and a later deposit is not added to it; a deposit below the sell's minimum may not complete the sale. The payout follows what arrives: Bridge takes the 1 % fee from it, rounded up to the cent, and, on a currency conversion, converts at its own rate at that moment, so the payout can differ from the quote. Crypto sent to another address or on another network may be lost. The sell waits for its deposit until `deposit_instructions.expires_at` (24 hours after the order was created), then it is cancelled — that is the sell's only deadline; `Order` has no `expires_at`. One open transfer per deposit address (2026-10-28): Bridge gives the customer one deposit address per network and reuses it, so a second open sell of the same customer on the same network and asset, whatever its amount, is `409 conflict`, `code: order_already_open`, and a sell while that address is held by something that is not an open sell order — a Pay an invoice payout being funded on that network, an earlier sell on it that Bridge has not yet confirmed, or a Swaps Wallet withdrawal (USDC on Tempo) waiting for its deposit — is `409 conflict`, `code: deposit_in_use` (see `409`). The other way round, `POST /v1/payouts/{id}/fund` and `POST /v1/wallet/offramp_intents` refuse while a sell holds the address. The daily and per-order risk ceilings apply in the SOURCE asset — the crypto being sold, not the fiat payout — and are checked on the quoted amount when the order is created; a larger deposit is still sold, even past a ceiling, and the amount received counts toward the daily ceiling for 24 hours from when it arrived. A sell is refused (`409 capability_unavailable`) for a payout rail outside the currently supported set (ACH, wire, SEPA, SPEI, Pix, Faster Payments); COP (`bre_b`) is not executable on this path, and `POST /v1/quotes` refuses a COP sell before pricing (`LOCAL_CURRENCY_NOT_SUPPORTED`, `param: to_asset`). `POST`/`GET /v1/wallet/external_accounts` create and list all six sell rails — `ach` and `wire` (USD), `sepa` (EUR), `spei` (MXN), `faster_payments` (GBP) and `pix` (BRL) — as bearer-only, `dark-flag` operations; a `sepa` account is created with its IBAN and its BIC, both required (`400 invalid_request` otherwise). The sell itself checks the account's currency and rail against the ROUTE; an account Swaps recorded as someone else's (a Pay an invoice or payroll recipient) answers `404` like a foreign id.

**Bridge-native buy rails (2026-10-20):** a buy on `pix`, `spei`, `bre_b`, `faster_payments` or `sepa` needs this customer's Bridge approval for that rail — `pix_onramp`, `spei`, `cop`, `faster_payments` or `sepa` — read live before anything is reserved or sent. Without it the order is refused `403 permission_error`, `code: endorsement_required`, `error.details: {payment_rail, endorsement, next}`, and nothing counts toward the daily cap. `next` is the customer's next step: `verify` (mint `POST /v1/customers/{id}/verification_links` with `kind: kyc` and this `rail`), `accept_terms` (a `kind: tos` link), `complete_profile` (a questionnaire the hosted flow does not collect), `provider_review` (Bridge is holding it; nothing to do) or `not_available` (Bridge does not offer the rail to this customer). Request a new quote once the step is done. USD rails (`ach_push`, `wire`) are not pre-checked. When Bridge itself refuses a buy's rail at transfer time, the same code answers — `next: verify` unless Bridge named the step, `endorsement` when known, and `error.details.order_id` when an order row was already written. A Bre-B (`bre_b`) buy's `bank_deposit_instructions` carry `bre_b_key`. A sell keeps its own gates.

**COP bank transfer — PSE (`co_bank_transfer`) through Bridge (D-123, 2026-10-20):** offered on `/v1` only to a caller that declared `hosted_handoff: true` on its quote (`QuoteRequest.hosted_handoff`; the buy widget v2 does) — only such a quote carries the offer; without it no PSE offer is priced, and the capability grid keeps the Bridge × `co_bank_transfer` cell `blocked` (`route_disabled`) because it is one answer for every client. Swaps' older checkouts (the legacy web widget, iOS) do not offer it until they support the hand-off. Gated on the same `cop` approval as Bre-B. Bridge collects it on its own hosted page, so the order carries no `bank_deposit_instructions`: the `201` carries `next_action` (`type: redirect`) — open `checkout_url` (Bridge's `start_url`) for the payer before `expires_at`; Bridge returns the payer to `return_url` afterwards. `return_url` is required for this rail: without it the order is refused `400 invalid_request` (`param: return_url`) before anything is read, reserved or sent. `GET /v1/orders/{id}` keeps publishing the same `next_action` while the order is `pending`/`awaiting_payment` and the page has not expired. Bridge's page host is not on a list this API keeps (the D-123 exception to the hosted-checkout host rule, founder ruling 2026-10-20): the URL comes from Bridge's own server-to-server response, and it is published only when it parses, is `https` and carries no user name or password. When it fails that, or Bridge answers no usable page, the Bridge transfer exists but cannot be paid here: `502 provider_error`, `code: provider_handoff_invalid`, `error.details.order_id` (see `502`). A Bridge `5xx` on the transfer itself is the ordinary Bridge-native `503` below (`error.details.order_id` when an order row was written) — never a `201`.

A `400` from this operation can also carry `code: 'bridge_execute_rejected'` (BSR-12, 2026-09-20) — Bridge rejects the selected offer's `execution_context.to_network` + wallet address for a quote this operation already priced and sold, because this route's own destination-network resolution never reaches the identical check `POST /v1/quotes` ran at quote time; a drift between the two surfaces here rather than a caller mistake. The envelope `type` stays `invalid_request` (this operation's `400` has no other canonical type); request a new quote and retry — do not treat it as the caller's own malformed request.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `IdempotencyKey` | component | — | — | Reusable parameter component |

### Request body

Required: **yes**

| Media type | Schema |
| --- | --- |
| `application/json` | [OrderCreateRequest](/reference/~schemas.md#ordercreaterequest) |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `201` | The created order. `Idempotent-Replayed` is present (`true`) when this response reconciled onto an existing order — created by a prior attempt under a different `Idempotency-Key`, recovered by the underlying provider-side idempotency key, OR (hosted provider, L1-6d) resumed by the pre-flight check for an active order already bound to this exact `quote_id` + `offer_id` — instead of creating a new one; absent on a genuinely fresh create. A response replayed under the SAME `Idempotency-Key` is the stored body verbatim, so its `next_action.expires_at` (when present) may already be in the past — check that field, or re-read `GET /v1/orders/{id}` for the current answer, rather than assume a replay is fresh. | `application/json` [Order](/reference/~schemas.md#order) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | The `quote_id` is not visible to this caller, or has already expired past recovery. On a sell, also the answer for a foreign or nonexistent `external_account_id` (L1-5, RESOURCE-MODEL §0.7, D-17) — byte-identical for both, deliberately: `403 permission_error` is never used for a cross-account id. | `application/json` [Error](/reference/~schemas.md#error) |
| `409` | State moved, the idempotency key was reused with a different body, or this corridor cannot serve the request (`capability_unavailable` — do not retry). On a sell (L1-5), `capability_unavailable` also covers: the payout rail is outside the currently supported set (`code: capability_unavailable`); the chosen external account's currency does not match `to_asset` (`code: external_account_currency_mismatch`); its rail does not match the route's payout rail, or could not be inferred at all (`code: external_account_rail_mismatch`); and Bridge's own structural pre-flight rejected the route/account/ customer combination before any reservation was taken (`code: offramp_route_unavailable`, `sideEffectFree: true`); and (2026-10-15) the quote's asset and `from_network` are not a pair a sell can be made on — Bridge's public offramp routes on a network Bridge accepts as a transfer source and Swaps can show an address for: USDC on arbitrum, avalanche, base, celo, ethereum, optimism, polygon, solana, stellar and tempo; USDT on ethereum, solana and tron; USDB on base (`code: capability_unavailable`, `sideEffectFree: true`). A sell may also be refused `conflict` with `code: order_already_open` (`sideEffectFree: true`): an order of the same customer on the same network and asset, whatever its amount, is still waiting for its deposit — Bridge reuses the customer's deposit address on a network, and an open transfer there takes the first deposit that arrives, whatever its amount (2026-10-28), so a second one would be ambiguous; `error.details.order_id` names the open order (send to it, or cancel it first). The check is atomic: while another order for the same deposit is still being created (seconds) the same refusal names that order once it exists, otherwise `error.details.quote_id` names this quote — list the orders and retry. Or with `code: deposit_in_use` (`sideEffectFree: true`, 2026-10-28): that deposit address is held by something that is not an open sell order, so `error.details.order_id` is never set — a Pay an invoice payout being funded on that network waits for its deposit (`error.details.payout_id` names it when it can be read: fund that payout, or cancel it with `POST /v1/payouts/{id}/cancel`, first); an earlier sell on that network is still being confirmed with Bridge (try again later; a retry of that same quote, offer and payout account is not refused by it and carries on with that sell); or a Swaps Wallet withdrawal on that network waits for its deposit (let it finish). Neither refusal creates anything: both come before any reservation and before any transfer is created at Bridge. On a hosted provider (L1-6c), `capability_unavailable` also covers: a hosted `side: sell` (no deposit flow yet); the offer's provider is not one this endpoint wires to (`code: capability_unavailable`); a Partna offer while `feature_flags.partna_quote_breakdown` is off (`code: provider_not_supported` — a Partna buy executes only while it is on, and it ships off, so until it is turned on every Partna offer is refused here); and a source currency with no explicit daily-cap ceiling row. `code: account_email_unavailable` is a hosted-only refusal: the resolved account has no verified email on file. Every one of these is `sideEffectFree: true` — none reaches the hosted checkout. `code: checkout_expired` (L1-6d) is a distinct `conflict` refusal, not `capability_unavailable`: an active order already exists for this exact `quote_id` + `offer_id`, but its stored checkout hand-off has expired, is absent, could not be decrypted, or no longer validates against this API's host allowlist — `error.details.order_id` names the existing order; request a new quote and create a new order rather than retry this one (`sideEffectFree: true`; no upstream provider call was made).<br>**Order-time executability and the next-ranked fallback (L1-6e):** every offer on the quote is independently classified before execution — `provider_paused` (Paybis), `provider_not_supported` (Partna while `feature_flags.partna_quote_breakdown` is off — it ships off, so until it is turned on every Partna offer is classified so), `capability_unavailable` (any other unreachable offer, including one this endpoint's own no-`offer_id` selection refuses to advertise or fall through to — a hosted offer whose `side` is not `buy`, or a `backend_native` offer whose provider is not Bridge, whose `side` is not `buy`/`sell`, or that is missing the network its side needs), or stale (`409 conflict`, `code: requote_required`, `error.details` absent) — the same three non-stale reasons `Order.offer_selection.skipped[].reason` can publish. A stale offer is NEVER added to `skipped[]`: an ABSENT-`offer_id` walk stops at the first one and answers `requote_required` immediately rather than falling through to a different, unwired provider for the same `quote_id` — this closes a double-submit a retried `Idempotency-Key` could otherwise reach. A PINNED `offer_id` that is not executable is refused with its own typed 409 above and `error.details.eligible_offer_ids` (up to five, in the server's own rank order, so a caller can re-post with one that will run) — this endpoint never substitutes a route the caller did not choose; a pinned offer whose only problem is the request-shape/network checks `createBridgeNativeOrder`/ `createHostedProviderOrder` themselves own (e.g. a missing `to_network`) still reaches that handler's own, more specific refusal — those stay authoritative for a PINNED request. An ABSENT `offer_id` walks `quote.offers` in that same rank order and executes the first offer that is BOTH independently classified executable AND structurally shaped for its execution path (the same hosted-`side`/`backend_native`- provider/side/network checks above, since selecting an offer no execution path could ever run would be false availability); when the executed offer is not the quote's own winning offer, the `201` names it in `offer_selection`. When no offer on the quote is executable, the refusal's `error.details` always carries both `eligible_offer_ids` and `skipped` (the same `{offer_id, provider_id, reason}` shape as `offer_selection.skipped`): every skipped offer sharing one reason answers with THAT reason and `eligible_offer_ids: []`; a genuinely mixed set of reasons answers the generic `capability_unavailable`, `eligible_offer_ids: []` — no single code would honestly describe it. `eligible_offer_ids` also excludes a hosted offer whose own `checkout_fresh_until`/`expires_at` has passed — advertising a stale one as "re-post with this and it will run" would be false availability too, even though the fallback walk itself still treats a stale HOSTED offer as reachable (the L1-6d pre-flight resume needs that). | `application/json` [Error](/reference/~schemas.md#error) |
| `413` | — | `PayloadTooLarge` |
| `422` | `amount_invalid`: either the sealed offer's amount exceeds the route's own ceiling (`resolved_limits.max`, further clamped by a config-owned risk ceiling; BSR-5), or its decimal precision exceeds the source asset's own scale (BSR-8) — defense in depth for a quote row minted before `POST /v1/quotes` started refusing that shape at its source. On a sell (L1-5) the source asset is the CRYPTO being sold — both the risk ceiling and the rolling 24-hour cap apply there, not to the fiat payout, and the cap message names the crypto currency. On a hosted provider (L1-6c), `amount_invalid` also covers: the payment method's own min/max, checked server-side at hand-off (`sideEffectFree: true`); and the rolling 24-hour value cap, scoped per account and currency across every hosted (non-Bridge) buy in the trailing 24h, not just this call's own reservation. `error.param` names `quote_id`. A hosted buy can also answer `travel_rule_originator_incomplete`, `travel_rule_proof_required` or `travel_rule_counterparty_required` — additional sender information is required before the transfer can proceed; NOT `sideEffectFree` (this fires after the order row is already written — reconcile via `GET /v1/orders`, `error.details.quote_id`, before retrying under a new key). | `application/json` [Error](/reference/~schemas.md#error) |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `502` | `provider_error`, `code: provider_handoff_invalid` (D-123): a Bridge-native COP PSE (`co_bank_transfer`) buy whose Bridge transfer was created, but whose hosted payment page Bridge returned is not publishable — its `start_url` does not parse, is not `https`, carries a user name or password, or names no host; its `expires_at` is unusable; or Bridge answered no `co_bank_transfer` instruction at all. `error.details.order_id` names the order, which cannot be paid through this API; it is not side-effect-free (a same-key replay answers `409 idempotency_failed`) — do not retry under a new Idempotency-Key, read `GET /v1/orders/{id}` (it publishes no `next_action` for it) and request a new quote. This needs operator attention. | `application/json` [Error](/reference/~schemas.md#error) |
| `503` | A kill switch is thrown or a dependency is out (`temporarily_unavailable`) — retry after `Retry-After`, when the header is present. A Bridge-native buy can fail partway through Step 5 (`handleBridgeExecuteTransfer`) after its local order — and, for some failure branches, the Bridge transfer itself — already exists (BSR-3, 2026-09-18); some of those branches carry `error.details.order_id` and some do not, shipping `error.details.quote_id` instead (BSR-13, 2026-09-20) — `BRIDGE_TRANSFER_RECONCILE_FAILED` proves a row exists via a `23505` unique violation but can still fail to read its id back, and the genuinely no-order-exists-yet tail never had an id to begin with. Neither of those two ships `Retry-After` any more either: the offer a `quote_id` prices has usually expired by the time any advertised delay would elapse (see the per-message breakdown below). Do not infer retry safety from `Retry-After`'s presence or absence on this operation — several other `temporarily_unavailable` branches from the SAME operation (an unreadable quote row, a stale gross-minimum snapshot, a route-claim sealing failure) are genuinely side-effect-free and still ship no `Retry-After` header.<br>BSR-3 fixer round 6 (independent review, 2026-09-18), correcting round 5's revision of this text: `code` DOES vary — `destination_check_unavailable` and `customer_status_unavailable` are distinct, genuinely side-effect-free `503`s from the pre-Bridge-execute checks (a same-key replay is safe for both); every other `503` below carries the generic `code: 'temporarily_unavailable'`. There is no `retryable` field anywhere in the `/v1` error envelope (an internal Bridge-side detail this contract deliberately keeps server-side). Check `code` first; treat `error.details.order_id`'s presence/absence as the primary "does an order exist" signal, not `message` alone — several strings below are shared by branches that differ only in whether `details` is set, and by very different Idempotency-Key dispositions:<br>  - `destination_check_unavailable`: "Could not verify this destination right now" or "Address screening<br>    is temporarily unavailable" — before any Bridge call; `Retry-After: 5`; side-effect-free; same-key<br>    replay is safe. Also "We couldn't check this address right now. Try again shortly." — the inbound<br>    rule's wallet or key-mark lookup failed for a buy into a wallet address, before any order row and any<br>    provider call; side-effect-free; a Bridge-native buy carries `error.details.quote_id` and no `Retry-After`.<br>  - `customer_status_unavailable`: "Could not verify this customer right now" — before any Bridge call;<br>    `Retry-After: 5`; side-effect-free; same-key replay is safe. (Fixer round 3 note, 2026-09-21 — this<br>    and the sibling above keep `Retry-After` on a STRUCTURAL argument only — earlier in the request<br>    usually means more offer TTL remains — not a measured one; BSR-14 tracks giving them the same<br>    treatment as the groups below if that stops holding.)<br>  - "Could not create this order — request a new quote and retry under a new Idempotency-Key" —<br>    `mapBridgeExecuteFailure`'s final unclassified tail only: no order exists yet. `Retry-After` is<br>    NEVER sent here (BSR-13, 2026-09-20 — was `Retry-After: 5`; LIVE evidence (dev, 3/3) proved a<br>    SAME-key replay after that advertised wait a dead end regardless — `409 idempotency_failed`,<br>    since this branch ships no `sideEffectFree` marker so the Idempotency-Key slot stays reserved).<br>    The message now names BOTH halves of the recovery explicitly (BSR-13 fixer round 1, 2026-09-21):<br>    a genuinely fresh `POST /v1/quotes` under a NEW Idempotency-Key — re-POSTing the SAME `quote_id`<br>    under only a new key instead re-prices a Bridge offer already partway through its own ~27-29s<br>    TTL by the time `handleBridgeExecuteTransfer` is reached, usually answering `409<br>    requote_required`/`404` on the offer/quote itself rather than the idempotency layer; a<br>    genuinely NEW quote's own offer TTL has not started counting down yet, so it does not have that<br>    problem. `error.details.quote_id` is set instead of `Retry-After`, so the caller has a handle to<br>    correlate this refusal with while getting that fresh quote.<br>  - "Could not create this order right now — nothing was created; retry with the same Idempotency-Key" —<br>    the gross-minimum/route-claim-sealing failures, before `handleBridgeExecuteTransfer` is called:<br>    side-effect-free, no `Retry-After`; a SAME-key replay is the correct retry. `error.details.quote_id`<br>    IS set (BSR-13 fixer round 3, 2026-09-21 — both shipped `details: {}` through round 2 even though<br>    `quoteRowId` was already in scope at both catches). Neither advertises a wait, so neither makes the<br>    broken-promise claim above — but "before `handleBridgeExecuteTransfer`" does not mean "before the<br>    offer TTL has run": both fire in the same last-few-steps window as the daily-value-cap check below,<br>    so the replay answers whatever the quote/offer's CURRENT state supports and can still 409<br>    `requote_required` on its own. BSR-14 tracks fixing this properly (longer offer TTL / re-pricing<br>    in place); this bullet only guarantees the Idempotency-Key semantics, not that the retry succeeds.<br>  - "Could not verify this account right now — request a new quote and retry" —<br>    `assertWithinBridgeNativeDailyCap`'s two structural-failure branches, taken at the LAST point<br>    before `handleBridgeExecuteTransfer`. `Retry-After` removed (BSR-13 fixer round 2, 2026-09-21 —<br>    was `Retry-After: 5`, the same broken-promise shape round 1 fixed on the tail above: the offer is<br>    already deep into its TTL by the time this runs). The unusable-inputs branch (before the RPC call)<br>    is genuinely side-effect-free. The RPC-failure branch is not provably so (BSR-13 fixer round 3,<br>    2026-09-21 — corrected from "nothing was reserved": the RPC inserts its reservation row before<br>    returning when it allows the request, so a failure observed after a committed call can leave a<br>    live reservation that self-expires on `BRIDGE_NATIVE_RESERVATION_TTL_SECONDS`). Both are safe on<br>    the Idempotency-Key axis either way — no order was created — so a SAME-key replay is correct; the<br>    message now names the real next step and `error.details.quote_id` gives a correlation handle.<br>  - "A transfer attempt for this request may already exist — check GET /v1/orders before retrying" —<br>    two different unclassifiable-outcome branches, both now setting `error.details.quote_id` (no<br>    `order_id` to name, but the quote is a real reconcile handle): `mapBridgeExecuteFailure`'s tail<br>    when a Bridge failure code proves (or cannot rule out) a row exists but no `transactionId` came<br>    back (`BRIDGE_TRANSFER_RECONCILE_FAILED` / `_INTENT_UNREADABLE` / `_RECOVERY_LOOKUP_FAILED`,<br>    BSR-13 2026-09-20); and a throw from `handleBridgeExecuteTransfer`'s own prologue (before its<br>    `try`/`catch` starts) — this one said nothing about which steps ran and shipped `details: {}`<br>    until BSR-13 fixer round 1 (2026-09-21) closed the same gap, since it fires strictly after the<br>    pre-Bridge-execute checks and `quoteRowId` is in scope there too. Neither carries `Retry-After`<br>    or is side-effect-free (a same-key replay answers `409`); poll `GET /v1/orders` before retrying<br>    under any key in both cases.<br>  - "Order was created but its id could not be determined — check GET /v1/orders shortly" — Bridge<br>    execute answered success with no `transactionId`; no `order_id` in `details`, but<br>    `error.details.quote_id` is set (BSR-13 fixer round 1, 2026-09-21 — this branch is reached only<br>    once Bridge execute has already succeeded, so a transactions row provably exists); poll<br>    `GET /v1/orders` (never a specific `{id}`).<br>  - "Order was created but could not be read back — check GET /v1/orders shortly" — the id IS known but<br>    this service's own read-back of it failed; `order_id` present; poll `GET /v1/orders/{id}`.<br>  - "Order was created but could not be confirmed — check GET /v1/orders shortly" — `order_id` present;<br>    poll `GET /v1/orders/{id}`, expected to resolve on its own.<br>  - "A compliance attestation for this order is incomplete (see order_id) — do not fund it yet. Check GET<br>    /v1/orders for the latest status." — `order_id` present; the transfer already committed and is not<br>    rejected; poll, but do not fund until it resolves.<br>  - "An order record exists for this attempt (see order_id); it may not be payable. Do not retry —<br>    contact support if it stays pending." — `order_id` present; a DEFINITIVE Bridge rejection with no<br>    mechanism to reach a terminal state on its own (BSR-20, github.com/swapsapp/swaps/issues/3312); never<br>    retry — a same-key retry answers `409`, a re-quoted retry mints a duplicate provider transfer.<br>  - "Could not verify this bank account right now" — sell only (L1-5): the external-account lookup at<br>    Bridge failed (never a 403/404, which mean the lookup itself succeeded); side-effect-free;<br>    `error.details.quote_id` set; no Bridge transfer was attempted. Distinct from the buy branch's<br>    "Could not verify this destination right now" (`destination_check_unavailable` above) — that one<br>    screens a crypto address and ships `Retry-After: 5`; this refuses a bank account and does not.<br>  - "Could not verify this route right now" — sell only (L1-5): the offramp dry-run pre-flight failed for<br>    a transport reason (not a clean route rejection, which is `409 offramp_route_unavailable` above);<br>    side-effect-free; `error.details.quote_id` set; no reservation was taken.<br>  - "Checkout could not be completed right now — check GET /v1/orders before retrying" — hosted provider<br>    (L1-6c) only: every unclassified outcome from the two internal hand-off calls, including a transport<br>    `502` and a provider-side failure at `internal_redirect_init` — by that point a `pending` `transactions`<br>    row and/or a live provider session may already exist. NEVER `sideEffectFree`; poll `GET /v1/orders`<br>    (`error.details.quote_id`) before retrying under any key, the same "reconcile, don't guess" rule the<br>    Bridge-native branches above already follow. | `application/json` [Error](/reference/~schemas.md#error) |

---

<a id="read-one-orders-state-and-money-truth-available"></a>
## `GET /orders/{id}` — Read one order's state and money truth — available

**Operation ID:** `orders.get`

> **Status:** Available · **Callers:** business key, agent (business key), dashboard session · **Scope:** `orders.read` · **Test mode:** unavailable

`status` is the public lifecycle. `money_state` is OPTIONAL — published only when the Paybis/Bridge failure-classification pipeline actually classified this row (BL-38, #3086); when present, read it with `money_status`: `money_state` says whether funds were ever captured, `money_status` carries refund truth — a classified row can read `status: completed` while `money_state` reads `never_authorized` (D-10). An ABSENT `money_state` means never classified — never read absence as `never_authorized` or as any other value. Cross-account ids are `not_found`, never `permission_error` (D-17).

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | `string` | — |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The order, current as of this read. | `application/json` [Order](/reference/~schemas.md#order) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | The id is not visible to this caller — also the answer for another account's order (D-17). | `application/json` [Error](/reference/~schemas.md#error) |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="cancel-an-order-before-it-settles-available"></a>
## `POST /orders/{id}/cancel` — Cancel an order before it settles — available

**Operation ID:** `orders.cancel`

> **Status:** Available · **Callers:** business key, agent (business key), dashboard session · **Scope:** `orders.write` · **Test mode:** unavailable

The live writer of `status: cancelled` (`transactions.cancel`, D-18). Cancelling cannot claw back money that already moved — check `money_state` first when it is present (BL-38, #3086: it is OPTIONAL, published only when classified). If `money_state` is absent, this row was never classified — that is NOT a green light to treat the order as unfunded; there is no refund operation because Swaps never initiates one (RESOURCE-MODEL §0.10). BL-48 narrows the success case beyond `status` alone: even a `status` that is otherwise cancellable is refused `409` when `money_state` has already been classified as `hold_placed` or `captured` — money moved or was reserved despite the row not yet having transitioned off a cancellable `status` (reconciliation lag). A `money_state` of `never_authorized`, or its absence (never classified), still cancels normally. BSR-2: for a Bridge-native buy this also asks Bridge to delete the upstream transfer before the local row moves — Bridge rows are never classified into `money_state` (it is Paybis-only today), so the dominant refusal for one of these orders is `409 cancel_unavailable` (Bridge itself refused: funds may already be in flight), not the `money_state` gate above. A transient Bridge outage answers `503 temporarily_unavailable` instead — nothing moved, retry after `Retry-After`. A payment-link pay-in order answers `409 payment_link_pay_in`: it belongs to the payer, cancel the link itself from the Payment Links screen instead. An order that funds a payout, or whose Bridge transfer belongs to a payout, answers `409 cancel_unavailable` before any provider call: it is cancelled only through the payout (`POST /v1/payouts/{id}/cancel`).

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | `string` | — |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `IdempotencyKey` | component | — | — | Reusable parameter component |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The order, now `status: cancelled`. | `application/json` [Order](/reference/~schemas.md#order) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `409` | One of four refusals, distinguished by `code`: `conflict` — the order already moved past a cancellable state (e.g. `status: completed`), re-read and decide; `cancel_unavailable` — the BL-48 `money_state` gate (`hold_placed`/`captured`), or, for a Bridge-native buy, Bridge itself refused to delete the transfer (funds may already be in flight), or this order funds a payout and is cancelled only through `POST /v1/payouts/{id}/cancel` (`error.details.payout_id` names that payout when this order is its funding order) — never retry as-is; `payment_link_pay_in` — this order is a payment-link pay-in, which belongs to the payer, not this caller — cancel the link itself instead; `reusable_bridge_template` — this order is a reusable Bridge deposit template, not a one-time transfer, and cancelling it here would destroy a template meant to be reused. | `application/json` [Error](/reference/~schemas.md#error) |
| `413` | — | `PayloadTooLarge` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | A kill switch is thrown, a dependency is out, or — for a Bridge-native buy (BSR-2) — Bridge itself timed out or 5xx'd while this cancel tried to delete the transfer. Nothing moved and nothing was deleted; retry after `Retry-After`. Also while a Bridge order is still being set up — its creation at Bridge is not resolved yet (the transfer may exist but is not attached to the order) — it is never cancelled locally; retry later. | `application/json` [Error](/reference/~schemas.md#error) |

---

<a id="list-one-orders-timeline-available"></a>
## `GET /orders/{id}/events` — List one order's timeline — available

**Operation ID:** `orders.events.list`

> **Status:** Available · **Callers:** business key, agent (business key), dashboard session · **Scope:** `orders.read` · **Test mode:** sandbox

`order.status_changed`, `.failed`, `.refunded`, `.cancelled` — lifecycle and `money_state` together, when `money_state` is present (it is OPTIONAL, published only when classified — BL-38, #3086) (RESOURCE-MODEL §3, D-92). A view over the K8 outbox (`api_events`) scoped to this order — there is no separate order-history table, so this reports exactly what the outbox recorded, never a value re-derived from the order's current row. While the `api_v1.events` outbox is off, this legitimately answers an empty page for an order that has moved through several statuses — it is reporting the outbox, not the order, and it never fabricates an event to fill the gap.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | `string` | — |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `Limit` | component | — | — | Reusable parameter component |
| `Cursor` | component | — | — | Reusable parameter component |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The order's event timeline, newest first. | `application/json` [OrderEventList](/reference/~schemas.md#ordereventlist) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |
