Quotes and orders
Buy and sell across the provider fan-out.
Jump to an operation:
Price a buy or a sell (swap is paused) — available
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).
Headers
Idempotency-Key^[A-Za-z0-9_-]{16,12… · requiredCaller-generated, 16-128 characters of [A-Za-z0-9_-] (a UUID or a base64url token both qualify). The value is part of a primary key, so the shape is enforced, not advisory: outside it the request is 400 invalid_request/idempotency_key_invalid. A replay within 24 hours returns the stored response; a different body under the same key is idempotency_error; a replay of a request that failed after it may have taken effect is 409 idempotency_failed — reconcile, then retry under a NEW key. After 24 hours the key is forgotten and the request is new (D-108).
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Price a buy or a sell (swap is paused) — available › Request Body
sideDirection of the trade. swap is crypto-to-crypto; search exists in the underlying contract but is never a public input (RESOURCE-MODEL §2.4, R15 §3).
from_assetSource asset symbol or ISO 4217 fiat code (bestQuoteRequestSchema.fromAsset).
to_assetDestination asset symbol or ISO 4217 fiat code (bestQuoteRequestSchema.toAsset).
from_amount^[0-9]{1,15}(\.[0-9]…Amount to spend, as a decimal string in from_asset's major units — must be strictly positive; the router rejects zero and validates it against the route's resolved minimum (bestQuoteRequestSchema.fromAmount). Exactly one of from_amount / to_amount is given; the router quotes the other side. Bounded by digit count, not a flat character cap (SEC/BSR-5, buy/sell v-fix review 2026-09-17; widened fixer round 1, 2026-09-19) — this string previously had no length cap at all: from_amount "99999999999999999999" (20 digits) reached a live Bridge backend_native offer and echoed back as final_in: "9999999999999999999900" minor units. A flat maxLength: 18 closed that but was itself too narrow: prod quote_attempts (last 30 days, widget-originated but pricing the SAME shape an /v1 destination-amount caller sends) has real rows up to 20 characters, e.g. 0.03853376279604058 (19) and 0.004231894472103885 (20) — full-precision crypto amounts an ERC-20 caller pricing by destination amount routinely sends. The pattern instead bounds the INTEGER part to 15 digits — one above the widest documented per-currency ceiling (CURRENCY_MAX_AMOUNT_MAJOR_UNITS.COP, packages/config/amountLimits.ts, 14 integer digits at 0 decimals) — and the FRACTIONAL part to 18 digits, comfortably covering every real value above while still rejecting the original 20-digit integer abuse case. This is a contract-layer SHAPE bound only, not a value ceiling: the per-offer/per-route VALUE limit (resolved_limits.max, further clamped by a config-owned risk ceiling) is enforced separately in createBridgeNativeOrder (supabase/functions/api-v1/routes/orders.ts) — a 15-digit integer string can still spell an amount far above a route's own limit without tripping a pattern/length check alone. Separately, for side: 'buy' only, POST /v1/quotes also refuses (422 amount_invalid) a from_amount whose fractional part carries more precision than from_asset's OWN scale (BSR-8, getPrecision — packages/config/amountLimits.ts's CURRENCY_PRECISION, the canonical fiat-decimals table .claude/rules/money.md names for every amount limit in this codebase) — e.g. "20.999" for a 2-decimal fiat currency like EUR — a shape this pattern deliberately still admits (18 fractional digits, for a real 18-decimal crypto amount) but that no fiat rail can move: LIVE evidence (dev) showed a Bridge backend_native offer round that exact value to EUR 21.00 internally while sealing the raw "20.999" verbatim into the deposit instructions, instructing the payer to send an amount SEPA cannot carry. A value with harmless trailing zeros beyond the scale ("20.990") is unaffected, as is a real 3-decimal Gulf dinar ("1.001" KWD/BHD/OMR/JOD — CURRENCY_PRECISION lists these at 3, not the naive 2-decimal guess an earlier cut of this fix used). This scale check applies to from_amount only, never to_amount (see that field's own description for why), and to side: 'buy' only, never sell/swap: sell's from_asset is the crypto being sold and swap is crypto-to-crypto, neither a fiat amount CURRENCY_PRECISION has an opinion on — an unlisted ERC-20 (e.g. DAI, 18 decimals) checked against getPrecision's own 2-decimal fallback would be misjudged and refused for a perfectly real amount.
to_amount^[0-9]{1,15}(\.[0-9]…Amount to receive, as a decimal string in to_asset's major units — must be strictly positive, when the caller is pricing by destination amount instead of source amount (bestQuoteRequestSchema.toAmount). Bounded the same way as from_amount (SEC/BSR-5, widened fixer round 1) — see that field's description for why. Deliberately NOT held to from_amount's BSR-8 per-asset scale check: from_amount (or the offer's own final_in derived from it, when priced in receive-mode) is the one figure this gateway ever seals into a live payer-facing deposit instruction, and to_amount is not — no route this API exposes today seals a raw to_amount into anything a payer must send or a rail must move. A caller pricing BY destination amount routinely sends the full on-chain precision of what it wants to receive (prod quote_attempts: to_amount: "0.004231894472103885", 18 fractional digits, for to_asset: "USDC", a 6-decimal token) — the SmartRouter still prices that correctly into a from-leg the provider itself scales properly, so there is no verbatim-sealing hazard on this side to close.
country^[A-Z]{2}$ISO 3166-1 alpha-2 country of the payer, when known — narrows the payment-method fan-out (bestQuoteRequestSchema.country). A well-formed but sanctioned or unrecognized code is refused with 409 capability_unavailable, not accepted and quoted (BSR-6, SEC-02) — checked against the same catalog GET /v1/capabilities?product=buy_sell validates country against.
payment_methodPreferred payment method id, when known (bestQuoteRequestSchema.paymentMethod). Omit to let the router rank every connected method.
from_networkSource chain, for a crypto leg (bestQuoteRequestSchema.fromNetwork).
to_networkDestination chain, for a crypto leg (bestQuoteRequestSchema.toNetwork). Optional here, but a Bridge-native orders.create (handoff_mode: backend_native) requires the WINNING offer's own execution_context.to_network to be set — a quote priced without it cannot become a Bridge-native order (409 capability_unavailable). Set it explicitly for any asset with more than one public chain.
region^[A-Z0-9]{1,3}$The person's sub-national region, an uppercase ISO 3166-2 subdivision code without the country prefix (e.g. CA, NY), as on GET /v1/quote_estimates. Read only for country=US, where it must be a US state, DC or outlying-area code (otherwise 400, param: region); ignored for every other country. Send the same value the estimate used, so a provider that prices a US route only once it knows the state (Stripe Onramp) can quote the offer the estimate showed. A hint, not proof of residence — the provider's own checks decide.
hosted_handoffD-123 — true declares that the caller creates its order through POST /v1/orders and follows that order's next_action to a provider-hosted payment page, including a Bridge-native order. Only then are offers priced whose payment can only be completed on such a page: today a Bridge buy by COP bank transfer — PSE (payment_method: co_bank_transfer), which POST /v1/orders answers with next_action and no bank_deposit_instructions. Absent or false: no such offer is priced (a PSE request then gets no Bridge offer); every other offer is unchanged. Send the same value on GET /v1/quote_estimates.
Price a buy or a sell (swap is paused) — available › Responses
The best executable route, plus the full per-provider fan-out.
id^qt_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectsideThe winning route's provider (D-26).
rateDecimal string, from_asset per to_asset (BSR-9) — the server's own final_in's decimal value ÷ final_out's decimal value for the winning offer, computed BEFORE either is rounded into the published Money fields below (when final_in is absent it equals the request's from_amount), in the SAME two request currencies, computed identically for every offer in this response regardless of provider or side (never a provider's own internal pricing field — Bridge's and Coinbase's own rate math disagree on this basis by side, which is exactly the bug BSR-9 closed: docs/api/CHANGELOG.md). Because rate is derived pre-rounding, rate × final_out.amount (scaled by final_out.decimals) can differ from final_in.amount in the last significant digit(s) when an asset's published Money.decimals is smaller than the server's own internal precision — do not treat that identity as exact. Informational only — NEVER compare offers by rate (rounding/precision differ by asset). Compare by final_out for a from_amount request (every offer targets a different final_out for the same pay amount). For a to_amount request, compare by final_in only among offers whose final_out equals the requested to_amount (lower final_in is better there) — an offer whose final_out differs is not comparable this way; not every provider honors an exact-output target.
What the caller receives (bestQuoteResponseSchema.finalOut).
Fee breakdown, field names from quoteFeesSchema (packages/contracts-exchange/schemas.ts). Each leg converts to a minor-unit Money object at the /v1 boundary per RESOURCE-MODEL §0.3; network_currency (the zod schema's optional currency-of-the-network-fee field) is folded into network.currency rather than published twice.
expires_atQuotes expire — re-quote rather than reusing a stale one.
Field names from quoteOfferSchema.checkoutReadiness — tells the caller how the eventual order will complete.
quote_statusindicative came from a warm cache without running the live fan-out — re-quote live before acting on it once older than the safety window (bestQuoteResponseSchema.quoteStatus).
offer_idOpaque identifier of the winning offer; preserve it verbatim for selection.
payment_method^[a-z][a-z0-9_]*$Canonical method priced for the winning offer. This is requested funding intent, not proof of the eventual received settlement rail. Absent means unknown.
What the caller pays, when it differs from the request amount (bestQuoteResponseSchema.finalIn).
Field names from resolvedLimitsSchema. min/max share one currency rather than each carrying their own Money object, matching the zod source's flat shape verbatim — RESOURCE-MODEL §2.4 lists this field as resolved_limits{min,max,currency,source}, not as two nested Money objects. A1-4 — this schema is ALSO Capabilities.defaults.resolved_limits' shape (a producer outside this item's file ownership); min/max gained a pattern here rather than becoming Money objects, so that other producer's existing plain-decimal output is not broken by a shape change it was never part of fixing.
eta_secondskyc_levelquoteOfferSchema.kycLevel — the identity tier this route requires, when known.
Field names from routeFactsSchema — comparison context for a poor-value or no-KYC-eligible alternative.
indicative_as_ofWhen the indicative price was actually fetched — never "now" (bestQuoteResponseSchema.indicativeAsOf).
Every fully priced offer from the same call, including sibling payment methods and providers. This additive list is the priced method surface; providers[] remains the terminal one-row-per-provider diagnostic summary for backward compatibility. An offer omitted from this list was not safely priced and must not be inferred from capabilities — including when the reason is this gateway being unable to verify that offer's own currency/asset decimal scale (fixer round 1, 2026-09-22), a money reason, not a rate one. If payment_method is absent, the provider did not return a canonical method and the client must not present that entry as a selectable named method.
Terminal outcomes from this call's existing provider offers and diagnostics (D-21, BS-G2). No additional provider requests are made to populate these rows. Reasons are public codes; raw diagnostics and provider payloads are never exposed.
price_change_threshold_bpsThe basis-point move that triggers a re-quote prompt on the client (today a server constant, 50) — D-27.
Read a previously created quote — available
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.
path Parameters
id^qt_ · requiredHeaders
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Read a previously created quote — available › Responses
The quote as it was created.
id^qt_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectsideThe winning route's provider (D-26).
rateDecimal string, from_asset per to_asset (BSR-9) — the server's own final_in's decimal value ÷ final_out's decimal value for the winning offer, computed BEFORE either is rounded into the published Money fields below (when final_in is absent it equals the request's from_amount), in the SAME two request currencies, computed identically for every offer in this response regardless of provider or side (never a provider's own internal pricing field — Bridge's and Coinbase's own rate math disagree on this basis by side, which is exactly the bug BSR-9 closed: docs/api/CHANGELOG.md). Because rate is derived pre-rounding, rate × final_out.amount (scaled by final_out.decimals) can differ from final_in.amount in the last significant digit(s) when an asset's published Money.decimals is smaller than the server's own internal precision — do not treat that identity as exact. Informational only — NEVER compare offers by rate (rounding/precision differ by asset). Compare by final_out for a from_amount request (every offer targets a different final_out for the same pay amount). For a to_amount request, compare by final_in only among offers whose final_out equals the requested to_amount (lower final_in is better there) — an offer whose final_out differs is not comparable this way; not every provider honors an exact-output target.
What the caller receives (bestQuoteResponseSchema.finalOut).
Fee breakdown, field names from quoteFeesSchema (packages/contracts-exchange/schemas.ts). Each leg converts to a minor-unit Money object at the /v1 boundary per RESOURCE-MODEL §0.3; network_currency (the zod schema's optional currency-of-the-network-fee field) is folded into network.currency rather than published twice.
expires_atQuotes expire — re-quote rather than reusing a stale one.
Field names from quoteOfferSchema.checkoutReadiness — tells the caller how the eventual order will complete.
quote_statusindicative came from a warm cache without running the live fan-out — re-quote live before acting on it once older than the safety window (bestQuoteResponseSchema.quoteStatus).
offer_idOpaque identifier of the winning offer; preserve it verbatim for selection.
payment_method^[a-z][a-z0-9_]*$Canonical method priced for the winning offer. This is requested funding intent, not proof of the eventual received settlement rail. Absent means unknown.
What the caller pays, when it differs from the request amount (bestQuoteResponseSchema.finalIn).
Field names from resolvedLimitsSchema. min/max share one currency rather than each carrying their own Money object, matching the zod source's flat shape verbatim — RESOURCE-MODEL §2.4 lists this field as resolved_limits{min,max,currency,source}, not as two nested Money objects. A1-4 — this schema is ALSO Capabilities.defaults.resolved_limits' shape (a producer outside this item's file ownership); min/max gained a pattern here rather than becoming Money objects, so that other producer's existing plain-decimal output is not broken by a shape change it was never part of fixing.
eta_secondskyc_levelquoteOfferSchema.kycLevel — the identity tier this route requires, when known.
Field names from routeFactsSchema — comparison context for a poor-value or no-KYC-eligible alternative.
indicative_as_ofWhen the indicative price was actually fetched — never "now" (bestQuoteResponseSchema.indicativeAsOf).
Every fully priced offer from the same call, including sibling payment methods and providers. This additive list is the priced method surface; providers[] remains the terminal one-row-per-provider diagnostic summary for backward compatibility. An offer omitted from this list was not safely priced and must not be inferred from capabilities — including when the reason is this gateway being unable to verify that offer's own currency/asset decimal scale (fixer round 1, 2026-09-22), a money reason, not a rate one. If payment_method is absent, the provider did not return a canonical method and the client must not present that entry as a selectable named method.
Terminal outcomes from this call's existing provider offers and diagnostics (D-21, BS-G2). No additional provider requests are made to populate these rows. Reasons are public codes; raw diagnostics and provider payloads are never exposed.
price_change_threshold_bpsThe basis-point move that triggers a re-quote prompt on the client (today a server constant, 50) — D-27.
Estimate a buy or sell price without signing in — dark-flag
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 answers503 temporarily_unavailable; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers409 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.
query Parameters
sideDirection: buy (fiat → asset) or sell (asset → fiat). swap answers 409 capability_unavailable.
from_asset^[A-Za-z0-9][A-Za-z0… · requiredWhat 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^[A-Za-z0-9][A-Za-z0… · requiredWhat 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).
country^[A-Z]{2}$ · requiredThe country the person picked, uppercase ISO 3166-1 alpha-2. It narrows the providers and methods; it is not proof of residence.
from_network^[a-z0-9][a-z0-9_-]{…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^[a-z0-9][a-z0-9_-]{…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^[0-9]{1,15}(\.[0-9]…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^[0-9]{1,15}(\.[0-9]…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.
payment_method^[a-z][a-z0-9_]{0,63…Price one payment method only. Omit to estimate every method the route offers.
region^[A-Z0-9]{1,3}$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_handoffD-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.
Headers
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Estimate a buy or sell price without signing in — dark-flag › Responses
The estimate. Sent with Cache-Control: no-store — request a new one rather than replaying a stored copy.
objectlivemodetrue on production, where every estimate is priced by live providers. The development project, which serves provider sandboxes, answers false. An anonymous caller has no test mode of its own.
quote_statusAlways indicative — an estimate is a historical price reference, never an executable quote.
indicative_as_ofWhen the providers actually priced this — never "now".
expires_atWhen this estimate becomes too old to show: indicative_as_of plus the longest age a cached price is ever replayed (330 seconds). It is not a provider's quote deadline; request a new estimate after it, and a binding quote before acting.
Every provider and payment method that priced this route in the cached fan-out, in the server's price-first order. Paybis is never included, and on a sell only Bridge is (see providers). An empty list never reaches the wire — a route with nothing to show is 409 capability_unavailable.
Only the providers that priced this route in the cached fan-out (quoted), plus Paybis as paused when it priced it, because /v1 never offers Paybis — and, on a sell, any provider other than Bridge as paused, because only Bridge sells through /v1 today. It is no availability claim for any other provider: one that is absent produced no usable cached price, and the estimate does not say why — a binding quote reports every provider's own outcome.
Only on GET /v1/quote_estimates/first_screen: the market the server chose from the country's coverage defaults and priced — key the estimate by it. Absent on GET /v1/quote_estimates, whose caller named the market.
Estimate a country's first-screen buy price, with the market chosen from its coverage — dark-flag
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 answers503 temporarily_unavailable; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers409 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).
query Parameters
country^[A-Z]{2}$ · requiredThe country the person picked, uppercase ISO 3166-1 alpha-2. Its coverage defaults choose the market; it is not proof of residence.
region^[A-Z0-9]{1,3}$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_handoffAs on GET /v1/quote_estimates (D-123). Any other value than true or false is 400.
Headers
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Estimate a country's first-screen buy price, with the market chosen from its coverage — dark-flag › Responses
The estimate, with first_screen naming the market priced. Sent with Cache-Control: no-store.
objectlivemodetrue on production, where every estimate is priced by live providers. The development project, which serves provider sandboxes, answers false. An anonymous caller has no test mode of its own.
quote_statusAlways indicative — an estimate is a historical price reference, never an executable quote.
indicative_as_ofWhen the providers actually priced this — never "now".
expires_atWhen this estimate becomes too old to show: indicative_as_of plus the longest age a cached price is ever replayed (330 seconds). It is not a provider's quote deadline; request a new estimate after it, and a binding quote before acting.
Every provider and payment method that priced this route in the cached fan-out, in the server's price-first order. Paybis is never included, and on a sell only Bridge is (see providers). An empty list never reaches the wire — a route with nothing to show is 409 capability_unavailable.
Only the providers that priced this route in the cached fan-out (quoted), plus Paybis as paused when it priced it, because /v1 never offers Paybis — and, on a sell, any provider other than Bridge as paused, because only Bridge sells through /v1 today. It is no availability claim for any other provider: one that is absent produced no usable cached price, and the estimate does not say why — a binding quote reports every provider's own outcome.
Only on GET /v1/quote_estimates/first_screen: the market the server chose from the country's coverage defaults and priced — key the estimate by it. Absent on GET /v1/quote_estimates, whose caller named the market.
List this holder's buy, sell and swap orders — available
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.
query Parameters
limitDefault 25. Out of range is never rejected: a value outside 1..100, a fraction, or a non-number is not a 400 — 0, a negative number, a non-integer or anything non-numeric falls back to the default (25); anything above 100 is capped to 100. A documented clamp, not a silent one; minimum/maximum are deliberately absent from this schema so a generated client (the MCP tool included) does not reject a value the API itself accepts.
cursorOpaque cursor from a previous response's next_cursor.
status_groupHeaders
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
List this holder's buy, sell and swap orders — available › Responses
A page of orders, newest first.
has_morenext_cursorA1-2: required (still nullable) — every producer already emits null rather than omitting the field when there is no next page, so this only removes a third, unused wire state (absent) a strict client had to handle for nothing.
Create an order from a quote — available
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-Keys) 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.
Headers
Idempotency-Key^[A-Za-z0-9_-]{16,12… · requiredCaller-generated, 16-128 characters of [A-Za-z0-9_-] (a UUID or a base64url token both qualify). The value is part of a primary key, so the shape is enforced, not advisory: outside it the request is 400 invalid_request/idempotency_key_invalid. A replay within 24 hours returns the stored response; a different body under the same key is idempotency_error; a replay of a request that failed after it may have taken effect is 409 idempotency_failed — reconcile, then retry under a NEW key. After 24 hours the key is forgotten and the request is new (D-108).
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Create an order from a quote — available › Request Body
quote_id^qt_ · requiredA Quote.id from a prior POST /v1/quotes.
offer_idSelects a specific QuoteProviderResult offer instead of the winning route, when the caller reviewed the fan-out and picked a different provider.
external_account_id^ba_REQUIRED for a Bridge-native sell (execution_context.side: sell on a backend_native offer): the saved bank destination the fiat payout settles to, from GET /v1/wallet/external_accounts. It must belong to this account's own Bridge customer, and its currency and rail must match the quote's to_asset and the offer's payout rail — a mismatch is refused, never reinterpreted. Never accepted on a buy or swap.
wallet_addressCrypto destination for a buy. NOT accepted on a Bridge-native sell (400 invalid_request, param: wallet_address): Bridge matches the incoming deposit from ANY sending wallet, so a declared source address would be a promise the server cannot keep. Send from any wallet you control to the address in the created order's deposit_instructions. With address_book_id, omit it, or send the entry's address byte for byte (addresses are case-sensitive and never case-folded); any other value is 400 invalid_request, param: wallet_address.
address_book_id^adr_A buy destination from this caller's own address book (GET /v1/address_book), instead of a typed wallet_address: the server reads the address and its network from the entry. Dashboard-session callers only — a business key is refused (403 permission_error, scope_denied), as it is on every /v1/address_book operation. An id that is not this caller's own is 404. The entry must be rail: crypto, saved for the offer's execution_context.to_network (network names are matched the way screenings match them — Ethereum, eth and 1 are one network; base is another), and its latest screening must not be block (403 destination_blocked); anything else is 400 invalid_request, param: address_book_id. Not accepted on a sell. The destination is still screened at order time exactly like a typed address. After a 201, the entry's last_used_at is set.
return_urlWhere the hosted provider sends the payer back after checkout. https only, at most 2048 characters, no user name or password, no query string and no fragment (put an order reference in the path; no personal data ever travels in a URL), and its origin must be one of https://swaps.app, https://www.swaps.app, https://staging.swaps.app. Anything else is 400 invalid_request, param: return_url — never replaced with a default. Absent, the provider returns the payer to the Swaps /success page. Signed into the checkout and checked again before the provider receives it. Used by providers that redirect back (Transak, OnMeta) and by a Bridge-native COP PSE buy (co_bank_transfer, D-123), where it is REQUIRED — Bridge sends the payer back there after its hosted bank-transfer page (absent: 400 invalid_request, param: return_url). Stripe Onramp's and Partna's hosted checkouts have no return redirect, and every other Bridge-native order has no hosted checkout, so for those it is validated and not used.
emailF-5: ignored. No path reads this field — a hosted checkout's identity is built from this account's own verified email (public.users), never from the request body, and the internal action this route calls has no email parameter to forward one to. Ignore any value sent here; do not build client logic on it being used. Kept on the wire in case a future provider path needs a caller-suggested address, not because one exists today.
Create an order from a quote — available › Responses
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.
id^ord_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectstatusA1-2: open — a new transactions.status value is a real, growing set (see K6/BL-36 history below); an unrecognized member is an opaque string, never a deserialization failure. transactions.status verbatim (D-22) — transactions/recovery.ts's ACTIVE_STATUSES + TERMINAL_STATUSES + UNCLASSIFIED_STATUSES vocabulary (19 values), widened from the seven the approved R15 §3 screens draw on (K6 fix: the original enum only covered the seven approved terminal/active screens and rejected every other real transactions.status value as a contract violation). BL-36 (2026-09-16) adds initiated (3 prod rows) and abandoned (1293 prod rows) — the DB status_check constraint has allowed both since 20260708201001_transactions_status_add_refunded.sql, but the vocabulary here and in recovery.ts never caught up: orders.get 500ed on either status (respondValidated rejects a status the enum does not list) and a page of orders.list containing one failed the same way whenever that row also carried a quote_id. abandoned joins TERMINAL_STATUSES; initiated is admitted here for contract purposes only and is deliberately left in recovery.ts's UNCLASSIFIED_STATUSES — prod shows every live initiated row already carries an on-chain tx_hash (one is metadata.lifecycle_stage: 'completed'), so it is a stranded post-broadcast swap the DEX reconciler never picked up, not a start-of-lifecycle "no money moved" state; see recovery.ts's comment for the full writer/reconciler trace. Never conflate with failure_kind — the failed screen's "Status code" must read failure_kind, not this field (BS-G9).
sidequote_id^qt_An opaque reference to the quote this order came from, when one exists. OPTIONAL — ABSENT, never a fabricated id, for an order that has no producer for it (a live write-path gap on some provider-native orders, or a swap that never went through a quote at all; see the API changelog for current prod scope). NOT resolvable via GET /v1/quotes/{id} for a historical order: this value can reference quote_attempts.rid, a different table than the one that endpoint reads, while an order created through POST /v1/orders carries a GET /v1/quotes/{id}-resolvable id instead.
rateDecimal string, from_asset per to_asset, the SAME basis as QuoteOffer.rate (BL-28, BS-10; fixer round 2, P1, independent review 2026-09-20 — round 1 published this on Bridge's own native, opposite basis, which collided with sibling PR BSR-9/#3357's provider-agnostic wire contract). BSR-10 gives this a live producer for a Bridge-native buy order: read rate_basis, below, to know which of two figures this is — never assume one from this field's mere presence. The planned read-time source (transactions.quote_id -> quotes.id -> quotes.offers) is a dead join in production — transactions.quote_id has referenced quote_attempts.rid, not quotes.id, since 20260210100000_fix_transactions_quote_fk.sql, and quote_attempts carries no rate/offers column. BL-48 re-examined this for orders.create's Bridge-native path (BL-47), which prices via api_quotes (a genuinely joinable row, unlike quotes) — still ruled out as a read-time source, since api_quotes is quote-cache infrastructure with its own TTL, and reading it back from orders.get/.list days after settlement, for a row a future cleanup job may have already pruned, would make this field's presence flicker on a purge schedule having nothing to do with the order itself. BSR-10 uses a WRITE-time source instead: createBridgeNativeOrder's execute-transfer call opens a server-sealed route claim and bridge-core.ts derives the persisted rate from THAT claim, inverted onto this wire basis (never from a request-body value), onto the transactions row itself at handoff (baseMetadata) — durable transaction history, never a re-derived read, and never a caller-supplied figure. Once settlement writes the observed to_amount (bridge-webhook/index.ts's transfer.payment_processed handler) AND Bridge's own transfer receipt proves the actual settled source amount, this field is RECOMPUTED as that receipt-proven source amount divided by the observed to_amount — a provider can legitimately re-price between quote and arrival for some corridors, so the settled figure is never assumed equal to the quoted one even when they agree; rate_basis flips to at_arrival in the same response. A row whose receipt never proves that source amount, or that reaches a terminal status without ever settling, publishes neither rate nor rate_basis rather than a stale or unproven figure. Still absent for every other order — a Sell/Swap order, a non-Bridge provider, or a Bridge buy this fix's write-time source never reached (a row created before this deploy) — never guessed.
rate_basisQualifies rate (above; both regimes share the same from_asset per to_asset basis). quoted: the taken offer's quote-time price, published only while the order is still active — the persisted estimate, never the final word; a terminal row without a receipt-proven settled figure publishes neither rate nor rate_basis (fixer round 3, P2, independent review 2026-09-20 — this paragraph had drifted to say the opposite of what buildOrderRateProjection actually gates on; rate's own description above, and RESOURCE-MODEL.md §2.4, already stated the gate correctly). at_arrival: the REALIZED rate, Bridge's own receipt-proven settled source amount divided by the observed amounts.receive once its transfer receipt lands — never assumed equal to the quoted figure, since a provider can legitimately re-price between quote and arrival for some corridors (Bridge's own "rate fixed on arrival" settlement semantics this field exists to name structurally, replacing a copy caveat with a field). Published alongside rate for a Bridge-native buy order (BSR-10); still absent alongside it for every other order, for the identical reason.
A provider's identity and, where the caller is pricing or reviewing capabilities, its registry-published limits — reused verbatim on Quote.provider, Order.provider and QuoteProviderResult (D-26, D-56). Capabilities.providers[] (product=buy_sell) uses CapabilityProvider below — a flat schema that duplicates these fields rather than an allOf extension of this one — so its own status vocabulary never collides with QuoteProviderResult.status, and so composing a second closed schema on top of this closed one never produces an unsatisfiable combined schema (K6b — see CapabilityProvider's own description).
payment_methodThe payment method originally requested for this order (transactions.payment_method, e.g. sepa/card/ach_push), verbatim — BL-48. Not restricted to a fiat leg: a Bridge buy funded from a crypto balance can carry a crypto method here too (e.g. bitcoin/ethereum) — prod carries 184 such rows. Absent when the row predates this column or the value was never set. Distinct from payment_rail: this is the REQUEST, not necessarily what settled — see that field.
payment_railBridge's own OBSERVED settlement rail for a Bridge order (transactions.payment_rail, BL-48) — starts equal to payment_method at creation and is overwritten once bridge-webhook/index.ts's buildBridgeTransferUpdatePayload resolves one (from transfer.created onward, not only once settled), so the two can genuinely diverge. Published ONLY for provider.id: bridge orders (fix-pack, independent review): the same column carries Transak's own private rail vocabulary (order.paymentOptionId, e.g. credit_debit_card) for a Transak order, which is not Bridge's vocabulary and disagreed with payment_method on the method itself on 448 prod rows — publishing it under this Bridge-documented field for another provider would misrepresent what the customer paid with. Never inferred — published verbatim from the column, or absent when it was never set or the provider is not Bridge.
The checkout's own money lines (R15 §4: "You pay" / "You receive"). The exact sub-field set is not yet spelled out beyond this pairing in RESOURCE-MODEL — flagged for confirmation rather than invented further. Fixer round 1 (2026-09-22) — either leg is WITHHELD, never published at an unverified decimal scale, when this gateway cannot verify that leg's currency/asset — see docs/api/CHANGELOG.md's entry of the same date. This rule is identical on every order.* event's own amounts (GET /v1/events, the SSE stream, GET /v1/activity) — each leg withholds independently there too, and amounts is absent from the event only when NEITHER leg resolved, never when just one could not be verified — see docs/api/CHANGELOG.md's L5-FIX-event-money-parity fixer round 1 entry (2026-09-22).
money_stateOPTIONAL — absent when this row has never been classified (BL-38, #3086): the writer is the Paybis/Bridge failure-classification pipeline, gated on failed/cancelled Paybis rows only, so most rows never receive one. When present, read it together with money_status: money_state says whether funds were ever captured, money_status carries refund truth. A row can read status: completed while money_state reads never_authorized (D-10). Never infer a money state from status alone, and never treat its absence as never_authorized — that is exactly the fabricated-default bug this field's optionality fixes.
money_statusDrives the "Money:" line on the terminal screens (R15 §3). Every bank rail resolves to unknown; only a card rail is ever positively not_charged.
failure_kindThe seven normalized failure kinds (R15 BS-G9, prod-inventory §1.4). This is the field the failed screen's "Status code" must read from — never status.
failure_reasonSafe, human-readable copy for failure_kind — never a raw provider error string.
return_reasonWhy a settled leg was sent back, as the provider itself worded it (e.g. Receiver Name Mismatch, Too Many Recent Transactions) — the returned leg is a fiat pay-in on a buy, and either the crypto deposit or the fiat payout on a Bridge-native sell (L1-5). Present only on an order whose money actually arrived and was then returned — distinct from failure_reason, which is OUR safe copy for a failure_kind. Same field and same contract as payments.return_reason on a payment link, so one integration handles both. Provider vocabulary is open-ended and NOT an enum: branch on failure_kind/status, and treat this as display and support text.
What the caller must do next before this order can advance, when anything is required of them at all. Present ONLY while a hosted-provider checkout is open and reachable: the order is in an active status, a hand-off URL exists for it, that URL has not expired, and its host is one this API recognises. ABSENT — never null, never a stale URL — the moment any of those stops being true, and on a Bridge-native order (which completes in place and publishes bank_deposit_instructions or deposit_instructions instead) — with ONE exception (D-123, 2026-10-20): a Bridge-native COP PSE buy (payment_rail: co_bank_transfer) is paid on Bridge's hosted page, so it carries next_action (Bridge's start_url and its expires_at) and no bank details, while the order is pending/awaiting_payment and the page has not expired. That URL's host is not on a list this API keeps (founder ruling 2026-10-20): it comes from Bridge's own server-to-server response and is published only when it parses, is https and carries no user name or password. A caller that receives no next_action on a hosted order whose status is still active must request a new quote; there is no way to re-mint a checkout session for an order that already has one. L1-6b (contract only, no producer yet): every route stays silent on this field until L1-6c/d build the hosted-checkout branch and its resume. Published by orders.create's 201 and GET /v1/orders/{id} only — GET /v1/orders never carries it, for any row.
offer_idThe offer actually executed for this order (transactions.offer_id), verbatim. Normally the offer_id the caller sent, or the quote's winning offer when none was sent; it can differ when the selected offer was not executable at order time and the server fell through to the next-ranked one — see offer_selection. Absent on an order created before this field existed or by a path that records no offer.
Why THIS offer ran, when that is not simply "the one you asked for". Published on the 201 from orders.create only — it is a record of a creation-time decision, and a later GET of the same order republishes offer_id but not this object. Absent when the executed offer is the one the request selected.
Present on a self-custody sell that needs crypto sent to a provider-issued address before payout — the shared DepositInstructions component, same shape as wallet.deposit_intents.deposit_instructions (D-24, BS-G7). On a Bridge sell the deposit deadline is deposit_instructions.expires_at (24 hours after the order was created); Order itself has no expires_at.
Present on a Buy order awaiting a bank-rail payment — the deposit slip (IBAN/account number, beneficiary, reference) the payer sends fiat to (BL-19). Reuses the shared BankDepositInstructions component verbatim rather than a second same-named shape, so this never drifts from the identical projection payment_sessions/payroll funding already publish. UNLIKE payout_destination below, this is deliberately the full, unmasked provider payload (D-24): the payer must have the real IBAN to actually send funds to it. Bound by the same D-24 rule as every BankDepositInstructions consumer — never logged, never placed in a URL or query string.
Present on a buy, sell or swap once its settlement destination is known (the buy crypto branch, BL-48, added below). The bank branch is masked the same way as payouts.beneficiary's output projection — never the full account number, never a public link or token (D-24); a sell order never itself becomes a payout object. The crypto branch (chain/address/tx_hash, BL-19; buy, BL-48) is exclusive with the bank branch: a self-custody payout is on-chain and has no account number to mask, so address is published verbatim (BS-G7's own deposit_instructions.address precedent). tx_hash on the buy branch is read from the SAME Bridge transfer receipt (bridge_data's receipt.destination_tx_hash) the amounts.fee producer reads (fix-pack, independent review — 66 of 70 completed Bridge buys carry one); absent before the transfer settles or on an older payload shape that predates the field, never guessed.
Where a Bridge-native sell stands, read from Bridge's own transfer state — a finer reading of status, never a second status. awaiting_deposit: Bridge is waiting for the crypto deposit (status: awaiting_payment). received: Bridge has received the deposit (processing). paying_out: Bridge has sent the bank payout (processing). paid: Bridge reports the bank payout processed (completed); when the money shows in the account is up to the receiving bank. Present only on a Bridge-native sell whose transfer state is one of those four and agrees with status. Absent otherwise — any other provider or side, a transfer state outside those four (a review, a return, a failure), or a state that disagrees with status. Absence says nothing about the money: read status, money_state and money_status.
Read one order's state and money truth — available
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).
path Parameters
id^ord_ · requiredHeaders
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Read one order's state and money truth — available › Responses
The order, current as of this read.
id^ord_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectstatusA1-2: open — a new transactions.status value is a real, growing set (see K6/BL-36 history below); an unrecognized member is an opaque string, never a deserialization failure. transactions.status verbatim (D-22) — transactions/recovery.ts's ACTIVE_STATUSES + TERMINAL_STATUSES + UNCLASSIFIED_STATUSES vocabulary (19 values), widened from the seven the approved R15 §3 screens draw on (K6 fix: the original enum only covered the seven approved terminal/active screens and rejected every other real transactions.status value as a contract violation). BL-36 (2026-09-16) adds initiated (3 prod rows) and abandoned (1293 prod rows) — the DB status_check constraint has allowed both since 20260708201001_transactions_status_add_refunded.sql, but the vocabulary here and in recovery.ts never caught up: orders.get 500ed on either status (respondValidated rejects a status the enum does not list) and a page of orders.list containing one failed the same way whenever that row also carried a quote_id. abandoned joins TERMINAL_STATUSES; initiated is admitted here for contract purposes only and is deliberately left in recovery.ts's UNCLASSIFIED_STATUSES — prod shows every live initiated row already carries an on-chain tx_hash (one is metadata.lifecycle_stage: 'completed'), so it is a stranded post-broadcast swap the DEX reconciler never picked up, not a start-of-lifecycle "no money moved" state; see recovery.ts's comment for the full writer/reconciler trace. Never conflate with failure_kind — the failed screen's "Status code" must read failure_kind, not this field (BS-G9).
sidequote_id^qt_An opaque reference to the quote this order came from, when one exists. OPTIONAL — ABSENT, never a fabricated id, for an order that has no producer for it (a live write-path gap on some provider-native orders, or a swap that never went through a quote at all; see the API changelog for current prod scope). NOT resolvable via GET /v1/quotes/{id} for a historical order: this value can reference quote_attempts.rid, a different table than the one that endpoint reads, while an order created through POST /v1/orders carries a GET /v1/quotes/{id}-resolvable id instead.
rateDecimal string, from_asset per to_asset, the SAME basis as QuoteOffer.rate (BL-28, BS-10; fixer round 2, P1, independent review 2026-09-20 — round 1 published this on Bridge's own native, opposite basis, which collided with sibling PR BSR-9/#3357's provider-agnostic wire contract). BSR-10 gives this a live producer for a Bridge-native buy order: read rate_basis, below, to know which of two figures this is — never assume one from this field's mere presence. The planned read-time source (transactions.quote_id -> quotes.id -> quotes.offers) is a dead join in production — transactions.quote_id has referenced quote_attempts.rid, not quotes.id, since 20260210100000_fix_transactions_quote_fk.sql, and quote_attempts carries no rate/offers column. BL-48 re-examined this for orders.create's Bridge-native path (BL-47), which prices via api_quotes (a genuinely joinable row, unlike quotes) — still ruled out as a read-time source, since api_quotes is quote-cache infrastructure with its own TTL, and reading it back from orders.get/.list days after settlement, for a row a future cleanup job may have already pruned, would make this field's presence flicker on a purge schedule having nothing to do with the order itself. BSR-10 uses a WRITE-time source instead: createBridgeNativeOrder's execute-transfer call opens a server-sealed route claim and bridge-core.ts derives the persisted rate from THAT claim, inverted onto this wire basis (never from a request-body value), onto the transactions row itself at handoff (baseMetadata) — durable transaction history, never a re-derived read, and never a caller-supplied figure. Once settlement writes the observed to_amount (bridge-webhook/index.ts's transfer.payment_processed handler) AND Bridge's own transfer receipt proves the actual settled source amount, this field is RECOMPUTED as that receipt-proven source amount divided by the observed to_amount — a provider can legitimately re-price between quote and arrival for some corridors, so the settled figure is never assumed equal to the quoted one even when they agree; rate_basis flips to at_arrival in the same response. A row whose receipt never proves that source amount, or that reaches a terminal status without ever settling, publishes neither rate nor rate_basis rather than a stale or unproven figure. Still absent for every other order — a Sell/Swap order, a non-Bridge provider, or a Bridge buy this fix's write-time source never reached (a row created before this deploy) — never guessed.
rate_basisQualifies rate (above; both regimes share the same from_asset per to_asset basis). quoted: the taken offer's quote-time price, published only while the order is still active — the persisted estimate, never the final word; a terminal row without a receipt-proven settled figure publishes neither rate nor rate_basis (fixer round 3, P2, independent review 2026-09-20 — this paragraph had drifted to say the opposite of what buildOrderRateProjection actually gates on; rate's own description above, and RESOURCE-MODEL.md §2.4, already stated the gate correctly). at_arrival: the REALIZED rate, Bridge's own receipt-proven settled source amount divided by the observed amounts.receive once its transfer receipt lands — never assumed equal to the quoted figure, since a provider can legitimately re-price between quote and arrival for some corridors (Bridge's own "rate fixed on arrival" settlement semantics this field exists to name structurally, replacing a copy caveat with a field). Published alongside rate for a Bridge-native buy order (BSR-10); still absent alongside it for every other order, for the identical reason.
A provider's identity and, where the caller is pricing or reviewing capabilities, its registry-published limits — reused verbatim on Quote.provider, Order.provider and QuoteProviderResult (D-26, D-56). Capabilities.providers[] (product=buy_sell) uses CapabilityProvider below — a flat schema that duplicates these fields rather than an allOf extension of this one — so its own status vocabulary never collides with QuoteProviderResult.status, and so composing a second closed schema on top of this closed one never produces an unsatisfiable combined schema (K6b — see CapabilityProvider's own description).
payment_methodThe payment method originally requested for this order (transactions.payment_method, e.g. sepa/card/ach_push), verbatim — BL-48. Not restricted to a fiat leg: a Bridge buy funded from a crypto balance can carry a crypto method here too (e.g. bitcoin/ethereum) — prod carries 184 such rows. Absent when the row predates this column or the value was never set. Distinct from payment_rail: this is the REQUEST, not necessarily what settled — see that field.
payment_railBridge's own OBSERVED settlement rail for a Bridge order (transactions.payment_rail, BL-48) — starts equal to payment_method at creation and is overwritten once bridge-webhook/index.ts's buildBridgeTransferUpdatePayload resolves one (from transfer.created onward, not only once settled), so the two can genuinely diverge. Published ONLY for provider.id: bridge orders (fix-pack, independent review): the same column carries Transak's own private rail vocabulary (order.paymentOptionId, e.g. credit_debit_card) for a Transak order, which is not Bridge's vocabulary and disagreed with payment_method on the method itself on 448 prod rows — publishing it under this Bridge-documented field for another provider would misrepresent what the customer paid with. Never inferred — published verbatim from the column, or absent when it was never set or the provider is not Bridge.
The checkout's own money lines (R15 §4: "You pay" / "You receive"). The exact sub-field set is not yet spelled out beyond this pairing in RESOURCE-MODEL — flagged for confirmation rather than invented further. Fixer round 1 (2026-09-22) — either leg is WITHHELD, never published at an unverified decimal scale, when this gateway cannot verify that leg's currency/asset — see docs/api/CHANGELOG.md's entry of the same date. This rule is identical on every order.* event's own amounts (GET /v1/events, the SSE stream, GET /v1/activity) — each leg withholds independently there too, and amounts is absent from the event only when NEITHER leg resolved, never when just one could not be verified — see docs/api/CHANGELOG.md's L5-FIX-event-money-parity fixer round 1 entry (2026-09-22).
money_stateOPTIONAL — absent when this row has never been classified (BL-38, #3086): the writer is the Paybis/Bridge failure-classification pipeline, gated on failed/cancelled Paybis rows only, so most rows never receive one. When present, read it together with money_status: money_state says whether funds were ever captured, money_status carries refund truth. A row can read status: completed while money_state reads never_authorized (D-10). Never infer a money state from status alone, and never treat its absence as never_authorized — that is exactly the fabricated-default bug this field's optionality fixes.
money_statusDrives the "Money:" line on the terminal screens (R15 §3). Every bank rail resolves to unknown; only a card rail is ever positively not_charged.
failure_kindThe seven normalized failure kinds (R15 BS-G9, prod-inventory §1.4). This is the field the failed screen's "Status code" must read from — never status.
failure_reasonSafe, human-readable copy for failure_kind — never a raw provider error string.
return_reasonWhy a settled leg was sent back, as the provider itself worded it (e.g. Receiver Name Mismatch, Too Many Recent Transactions) — the returned leg is a fiat pay-in on a buy, and either the crypto deposit or the fiat payout on a Bridge-native sell (L1-5). Present only on an order whose money actually arrived and was then returned — distinct from failure_reason, which is OUR safe copy for a failure_kind. Same field and same contract as payments.return_reason on a payment link, so one integration handles both. Provider vocabulary is open-ended and NOT an enum: branch on failure_kind/status, and treat this as display and support text.
What the caller must do next before this order can advance, when anything is required of them at all. Present ONLY while a hosted-provider checkout is open and reachable: the order is in an active status, a hand-off URL exists for it, that URL has not expired, and its host is one this API recognises. ABSENT — never null, never a stale URL — the moment any of those stops being true, and on a Bridge-native order (which completes in place and publishes bank_deposit_instructions or deposit_instructions instead) — with ONE exception (D-123, 2026-10-20): a Bridge-native COP PSE buy (payment_rail: co_bank_transfer) is paid on Bridge's hosted page, so it carries next_action (Bridge's start_url and its expires_at) and no bank details, while the order is pending/awaiting_payment and the page has not expired. That URL's host is not on a list this API keeps (founder ruling 2026-10-20): it comes from Bridge's own server-to-server response and is published only when it parses, is https and carries no user name or password. A caller that receives no next_action on a hosted order whose status is still active must request a new quote; there is no way to re-mint a checkout session for an order that already has one. L1-6b (contract only, no producer yet): every route stays silent on this field until L1-6c/d build the hosted-checkout branch and its resume. Published by orders.create's 201 and GET /v1/orders/{id} only — GET /v1/orders never carries it, for any row.
offer_idThe offer actually executed for this order (transactions.offer_id), verbatim. Normally the offer_id the caller sent, or the quote's winning offer when none was sent; it can differ when the selected offer was not executable at order time and the server fell through to the next-ranked one — see offer_selection. Absent on an order created before this field existed or by a path that records no offer.
Why THIS offer ran, when that is not simply "the one you asked for". Published on the 201 from orders.create only — it is a record of a creation-time decision, and a later GET of the same order republishes offer_id but not this object. Absent when the executed offer is the one the request selected.
Present on a self-custody sell that needs crypto sent to a provider-issued address before payout — the shared DepositInstructions component, same shape as wallet.deposit_intents.deposit_instructions (D-24, BS-G7). On a Bridge sell the deposit deadline is deposit_instructions.expires_at (24 hours after the order was created); Order itself has no expires_at.
Present on a Buy order awaiting a bank-rail payment — the deposit slip (IBAN/account number, beneficiary, reference) the payer sends fiat to (BL-19). Reuses the shared BankDepositInstructions component verbatim rather than a second same-named shape, so this never drifts from the identical projection payment_sessions/payroll funding already publish. UNLIKE payout_destination below, this is deliberately the full, unmasked provider payload (D-24): the payer must have the real IBAN to actually send funds to it. Bound by the same D-24 rule as every BankDepositInstructions consumer — never logged, never placed in a URL or query string.
Present on a buy, sell or swap once its settlement destination is known (the buy crypto branch, BL-48, added below). The bank branch is masked the same way as payouts.beneficiary's output projection — never the full account number, never a public link or token (D-24); a sell order never itself becomes a payout object. The crypto branch (chain/address/tx_hash, BL-19; buy, BL-48) is exclusive with the bank branch: a self-custody payout is on-chain and has no account number to mask, so address is published verbatim (BS-G7's own deposit_instructions.address precedent). tx_hash on the buy branch is read from the SAME Bridge transfer receipt (bridge_data's receipt.destination_tx_hash) the amounts.fee producer reads (fix-pack, independent review — 66 of 70 completed Bridge buys carry one); absent before the transfer settles or on an older payload shape that predates the field, never guessed.
Where a Bridge-native sell stands, read from Bridge's own transfer state — a finer reading of status, never a second status. awaiting_deposit: Bridge is waiting for the crypto deposit (status: awaiting_payment). received: Bridge has received the deposit (processing). paying_out: Bridge has sent the bank payout (processing). paid: Bridge reports the bank payout processed (completed); when the money shows in the account is up to the receiving bank. Present only on a Bridge-native sell whose transfer state is one of those four and agrees with status. Absent otherwise — any other provider or side, a transfer state outside those four (a review, a return, a failure), or a state that disagrees with status. Absence says nothing about the money: read status, money_state and money_status.
Cancel an order before it settles — available
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).
path Parameters
id^ord_ · requiredHeaders
Idempotency-Key^[A-Za-z0-9_-]{16,12… · requiredCaller-generated, 16-128 characters of [A-Za-z0-9_-] (a UUID or a base64url token both qualify). The value is part of a primary key, so the shape is enforced, not advisory: outside it the request is 400 invalid_request/idempotency_key_invalid. A replay within 24 hours returns the stored response; a different body under the same key is idempotency_error; a replay of a request that failed after it may have taken effect is 409 idempotency_failed — reconcile, then retry under a NEW key. After 24 hours the key is forgotten and the request is new (D-108).
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Cancel an order before it settles — available › Responses
The order, now status: cancelled.
id^ord_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectstatusA1-2: open — a new transactions.status value is a real, growing set (see K6/BL-36 history below); an unrecognized member is an opaque string, never a deserialization failure. transactions.status verbatim (D-22) — transactions/recovery.ts's ACTIVE_STATUSES + TERMINAL_STATUSES + UNCLASSIFIED_STATUSES vocabulary (19 values), widened from the seven the approved R15 §3 screens draw on (K6 fix: the original enum only covered the seven approved terminal/active screens and rejected every other real transactions.status value as a contract violation). BL-36 (2026-09-16) adds initiated (3 prod rows) and abandoned (1293 prod rows) — the DB status_check constraint has allowed both since 20260708201001_transactions_status_add_refunded.sql, but the vocabulary here and in recovery.ts never caught up: orders.get 500ed on either status (respondValidated rejects a status the enum does not list) and a page of orders.list containing one failed the same way whenever that row also carried a quote_id. abandoned joins TERMINAL_STATUSES; initiated is admitted here for contract purposes only and is deliberately left in recovery.ts's UNCLASSIFIED_STATUSES — prod shows every live initiated row already carries an on-chain tx_hash (one is metadata.lifecycle_stage: 'completed'), so it is a stranded post-broadcast swap the DEX reconciler never picked up, not a start-of-lifecycle "no money moved" state; see recovery.ts's comment for the full writer/reconciler trace. Never conflate with failure_kind — the failed screen's "Status code" must read failure_kind, not this field (BS-G9).
sidequote_id^qt_An opaque reference to the quote this order came from, when one exists. OPTIONAL — ABSENT, never a fabricated id, for an order that has no producer for it (a live write-path gap on some provider-native orders, or a swap that never went through a quote at all; see the API changelog for current prod scope). NOT resolvable via GET /v1/quotes/{id} for a historical order: this value can reference quote_attempts.rid, a different table than the one that endpoint reads, while an order created through POST /v1/orders carries a GET /v1/quotes/{id}-resolvable id instead.
rateDecimal string, from_asset per to_asset, the SAME basis as QuoteOffer.rate (BL-28, BS-10; fixer round 2, P1, independent review 2026-09-20 — round 1 published this on Bridge's own native, opposite basis, which collided with sibling PR BSR-9/#3357's provider-agnostic wire contract). BSR-10 gives this a live producer for a Bridge-native buy order: read rate_basis, below, to know which of two figures this is — never assume one from this field's mere presence. The planned read-time source (transactions.quote_id -> quotes.id -> quotes.offers) is a dead join in production — transactions.quote_id has referenced quote_attempts.rid, not quotes.id, since 20260210100000_fix_transactions_quote_fk.sql, and quote_attempts carries no rate/offers column. BL-48 re-examined this for orders.create's Bridge-native path (BL-47), which prices via api_quotes (a genuinely joinable row, unlike quotes) — still ruled out as a read-time source, since api_quotes is quote-cache infrastructure with its own TTL, and reading it back from orders.get/.list days after settlement, for a row a future cleanup job may have already pruned, would make this field's presence flicker on a purge schedule having nothing to do with the order itself. BSR-10 uses a WRITE-time source instead: createBridgeNativeOrder's execute-transfer call opens a server-sealed route claim and bridge-core.ts derives the persisted rate from THAT claim, inverted onto this wire basis (never from a request-body value), onto the transactions row itself at handoff (baseMetadata) — durable transaction history, never a re-derived read, and never a caller-supplied figure. Once settlement writes the observed to_amount (bridge-webhook/index.ts's transfer.payment_processed handler) AND Bridge's own transfer receipt proves the actual settled source amount, this field is RECOMPUTED as that receipt-proven source amount divided by the observed to_amount — a provider can legitimately re-price between quote and arrival for some corridors, so the settled figure is never assumed equal to the quoted one even when they agree; rate_basis flips to at_arrival in the same response. A row whose receipt never proves that source amount, or that reaches a terminal status without ever settling, publishes neither rate nor rate_basis rather than a stale or unproven figure. Still absent for every other order — a Sell/Swap order, a non-Bridge provider, or a Bridge buy this fix's write-time source never reached (a row created before this deploy) — never guessed.
rate_basisQualifies rate (above; both regimes share the same from_asset per to_asset basis). quoted: the taken offer's quote-time price, published only while the order is still active — the persisted estimate, never the final word; a terminal row without a receipt-proven settled figure publishes neither rate nor rate_basis (fixer round 3, P2, independent review 2026-09-20 — this paragraph had drifted to say the opposite of what buildOrderRateProjection actually gates on; rate's own description above, and RESOURCE-MODEL.md §2.4, already stated the gate correctly). at_arrival: the REALIZED rate, Bridge's own receipt-proven settled source amount divided by the observed amounts.receive once its transfer receipt lands — never assumed equal to the quoted figure, since a provider can legitimately re-price between quote and arrival for some corridors (Bridge's own "rate fixed on arrival" settlement semantics this field exists to name structurally, replacing a copy caveat with a field). Published alongside rate for a Bridge-native buy order (BSR-10); still absent alongside it for every other order, for the identical reason.
A provider's identity and, where the caller is pricing or reviewing capabilities, its registry-published limits — reused verbatim on Quote.provider, Order.provider and QuoteProviderResult (D-26, D-56). Capabilities.providers[] (product=buy_sell) uses CapabilityProvider below — a flat schema that duplicates these fields rather than an allOf extension of this one — so its own status vocabulary never collides with QuoteProviderResult.status, and so composing a second closed schema on top of this closed one never produces an unsatisfiable combined schema (K6b — see CapabilityProvider's own description).
payment_methodThe payment method originally requested for this order (transactions.payment_method, e.g. sepa/card/ach_push), verbatim — BL-48. Not restricted to a fiat leg: a Bridge buy funded from a crypto balance can carry a crypto method here too (e.g. bitcoin/ethereum) — prod carries 184 such rows. Absent when the row predates this column or the value was never set. Distinct from payment_rail: this is the REQUEST, not necessarily what settled — see that field.
payment_railBridge's own OBSERVED settlement rail for a Bridge order (transactions.payment_rail, BL-48) — starts equal to payment_method at creation and is overwritten once bridge-webhook/index.ts's buildBridgeTransferUpdatePayload resolves one (from transfer.created onward, not only once settled), so the two can genuinely diverge. Published ONLY for provider.id: bridge orders (fix-pack, independent review): the same column carries Transak's own private rail vocabulary (order.paymentOptionId, e.g. credit_debit_card) for a Transak order, which is not Bridge's vocabulary and disagreed with payment_method on the method itself on 448 prod rows — publishing it under this Bridge-documented field for another provider would misrepresent what the customer paid with. Never inferred — published verbatim from the column, or absent when it was never set or the provider is not Bridge.
The checkout's own money lines (R15 §4: "You pay" / "You receive"). The exact sub-field set is not yet spelled out beyond this pairing in RESOURCE-MODEL — flagged for confirmation rather than invented further. Fixer round 1 (2026-09-22) — either leg is WITHHELD, never published at an unverified decimal scale, when this gateway cannot verify that leg's currency/asset — see docs/api/CHANGELOG.md's entry of the same date. This rule is identical on every order.* event's own amounts (GET /v1/events, the SSE stream, GET /v1/activity) — each leg withholds independently there too, and amounts is absent from the event only when NEITHER leg resolved, never when just one could not be verified — see docs/api/CHANGELOG.md's L5-FIX-event-money-parity fixer round 1 entry (2026-09-22).
money_stateOPTIONAL — absent when this row has never been classified (BL-38, #3086): the writer is the Paybis/Bridge failure-classification pipeline, gated on failed/cancelled Paybis rows only, so most rows never receive one. When present, read it together with money_status: money_state says whether funds were ever captured, money_status carries refund truth. A row can read status: completed while money_state reads never_authorized (D-10). Never infer a money state from status alone, and never treat its absence as never_authorized — that is exactly the fabricated-default bug this field's optionality fixes.
money_statusDrives the "Money:" line on the terminal screens (R15 §3). Every bank rail resolves to unknown; only a card rail is ever positively not_charged.
failure_kindThe seven normalized failure kinds (R15 BS-G9, prod-inventory §1.4). This is the field the failed screen's "Status code" must read from — never status.
failure_reasonSafe, human-readable copy for failure_kind — never a raw provider error string.
return_reasonWhy a settled leg was sent back, as the provider itself worded it (e.g. Receiver Name Mismatch, Too Many Recent Transactions) — the returned leg is a fiat pay-in on a buy, and either the crypto deposit or the fiat payout on a Bridge-native sell (L1-5). Present only on an order whose money actually arrived and was then returned — distinct from failure_reason, which is OUR safe copy for a failure_kind. Same field and same contract as payments.return_reason on a payment link, so one integration handles both. Provider vocabulary is open-ended and NOT an enum: branch on failure_kind/status, and treat this as display and support text.
What the caller must do next before this order can advance, when anything is required of them at all. Present ONLY while a hosted-provider checkout is open and reachable: the order is in an active status, a hand-off URL exists for it, that URL has not expired, and its host is one this API recognises. ABSENT — never null, never a stale URL — the moment any of those stops being true, and on a Bridge-native order (which completes in place and publishes bank_deposit_instructions or deposit_instructions instead) — with ONE exception (D-123, 2026-10-20): a Bridge-native COP PSE buy (payment_rail: co_bank_transfer) is paid on Bridge's hosted page, so it carries next_action (Bridge's start_url and its expires_at) and no bank details, while the order is pending/awaiting_payment and the page has not expired. That URL's host is not on a list this API keeps (founder ruling 2026-10-20): it comes from Bridge's own server-to-server response and is published only when it parses, is https and carries no user name or password. A caller that receives no next_action on a hosted order whose status is still active must request a new quote; there is no way to re-mint a checkout session for an order that already has one. L1-6b (contract only, no producer yet): every route stays silent on this field until L1-6c/d build the hosted-checkout branch and its resume. Published by orders.create's 201 and GET /v1/orders/{id} only — GET /v1/orders never carries it, for any row.
offer_idThe offer actually executed for this order (transactions.offer_id), verbatim. Normally the offer_id the caller sent, or the quote's winning offer when none was sent; it can differ when the selected offer was not executable at order time and the server fell through to the next-ranked one — see offer_selection. Absent on an order created before this field existed or by a path that records no offer.
Why THIS offer ran, when that is not simply "the one you asked for". Published on the 201 from orders.create only — it is a record of a creation-time decision, and a later GET of the same order republishes offer_id but not this object. Absent when the executed offer is the one the request selected.
Present on a self-custody sell that needs crypto sent to a provider-issued address before payout — the shared DepositInstructions component, same shape as wallet.deposit_intents.deposit_instructions (D-24, BS-G7). On a Bridge sell the deposit deadline is deposit_instructions.expires_at (24 hours after the order was created); Order itself has no expires_at.
Present on a Buy order awaiting a bank-rail payment — the deposit slip (IBAN/account number, beneficiary, reference) the payer sends fiat to (BL-19). Reuses the shared BankDepositInstructions component verbatim rather than a second same-named shape, so this never drifts from the identical projection payment_sessions/payroll funding already publish. UNLIKE payout_destination below, this is deliberately the full, unmasked provider payload (D-24): the payer must have the real IBAN to actually send funds to it. Bound by the same D-24 rule as every BankDepositInstructions consumer — never logged, never placed in a URL or query string.
Present on a buy, sell or swap once its settlement destination is known (the buy crypto branch, BL-48, added below). The bank branch is masked the same way as payouts.beneficiary's output projection — never the full account number, never a public link or token (D-24); a sell order never itself becomes a payout object. The crypto branch (chain/address/tx_hash, BL-19; buy, BL-48) is exclusive with the bank branch: a self-custody payout is on-chain and has no account number to mask, so address is published verbatim (BS-G7's own deposit_instructions.address precedent). tx_hash on the buy branch is read from the SAME Bridge transfer receipt (bridge_data's receipt.destination_tx_hash) the amounts.fee producer reads (fix-pack, independent review — 66 of 70 completed Bridge buys carry one); absent before the transfer settles or on an older payload shape that predates the field, never guessed.
Where a Bridge-native sell stands, read from Bridge's own transfer state — a finer reading of status, never a second status. awaiting_deposit: Bridge is waiting for the crypto deposit (status: awaiting_payment). received: Bridge has received the deposit (processing). paying_out: Bridge has sent the bank payout (processing). paid: Bridge reports the bank payout processed (completed); when the money shows in the account is up to the receiving bank. Present only on a Bridge-native sell whose transfer state is one of those four and agrees with status. Absent otherwise — any other provider or side, a transfer state outside those four (a review, a return, a failure), or a state that disagrees with status. Absence says nothing about the money: read status, money_state and money_status.
List one order's timeline — available
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.
path Parameters
id^ord_ · requiredquery Parameters
limitDefault 25. Out of range is never rejected: a value outside 1..100, a fraction, or a non-number is not a 400 — 0, a negative number, a non-integer or anything non-numeric falls back to the default (25); anything above 100 is capped to 100. A documented clamp, not a silent one; minimum/maximum are deliberately absent from this schema so a generated client (the MCP tool included) does not reject a value the API itself accepts.
cursorOpaque cursor from a previous response's next_cursor.
Headers
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
List one order's timeline — available › Responses
The order's event timeline, newest first.
has_morenext_cursorA1-2: required (still nullable) — every producer already emits null rather than omitting the field when there is no next page, so this only removes a third, unused wire state (absent) a strict client had to handle for nothing.