Appearance
Error Handling & Retries
Every error returned by the API is classified as retryable or non-retryable so that your integration can decide, deterministically, whether to retry a failed request or treat it as final and compensate.
Errors are returned as the original error object — a business rejection carries the endpoint's own rejection body, while infrastructure checks (authentication, idempotency) use the error envelope; see Response Format for the shapes. This page adds the retry classification and organizes it by flow (quote & intent, off-ramp, payout).
Is an error retryable?
The rule follows the HTTP status class:
| Response | Cause | Retryable | Why |
|---|---|---|---|
5xx | Server / infrastructure — transient | Yes | Time resolves it — retry with backoff |
422 with status: "rejected" | Business rejection — liquidity, constraints, policy | No | You must change the request itself. Some subCodes are conditional — see Policy subCodes |
other 4xx | Client — request, credentials, or state | No | Same request yields the same result until you change it |
Two exceptions — 4xx that is retryable:
429(rate limited) — retry afterRetry-Afterwith exponential backoff.409(in progress) — the original request is still being processed. Retry with the sameIdempotency-Keyto re-query the result — never resend as a new request (see Payout).
One HTTP rule for rejections: a request that cannot produce a result never returns
200. Synchronous business rejections — no liquidity, constraints not met, policy denial — return422with the original rejection object (status: "rejected"plus a machine-readable reason).200/202always means the request produced a usable result (or was accepted for processing). Terminal failures of an already-accepted asynchronous flow (e.g. a settlement that later fails) surface as resource state on status reads and webhooks — not as an HTTP error on the original request.
One-line test: "Could the same request produce a different result if sent again unchanged?" Yes → retryable; No → not retryable.
Common errors (all endpoints)
| Code | HTTP | Retryable | Recommended action |
|---|---|---|---|
INVALID_API_KEY | 401 | No | Check credentials / rotate key |
INVALID_PARAMETER, INVALID_PAIR | 400 | No | Fix the request |
IDEMPOTENCY_KEY_REQUIRED, IDEMPOTENCY_KEY_INVALID | 400 | No | Supply a valid Idempotency-Key |
IDEMPOTENCY_KEY_CONFLICT | 409 | No | Same key reused with a different body — use a new key |
IDEMPOTENCY_KEY_PROCESSING | 409 | Yes (re-query) | Retry the same key to fetch the in-flight result |
RATE_LIMIT_EXCEEDED | 429 | Yes | Back off, honor Retry-After |
SERVICE_UNAVAILABLE, INTERNAL_ERROR | 5xx | Yes | Retry with exponential backoff |
Quote & Intent
Errors raised while pricing a quote or placing an intent. Business rejections return 422 — never 200 (see the rule above) — with this body:
json
{ "intentId": "…", "reason": "<code>", "status": "rejected" }Note that the intent-rejection body carries the reason at the top level (it is not wrapped in the standard error envelope). Malformed requests (missing or mis-shaped fields) return 400 with a human-readable message — those are not coded and are always non-retryable: fix the request.
Every rejection below happens before any on-chain lock — no funds have moved, and resubmitting a corrected request is always safe.
The reason values form a closed set — the full list as of this document version is below. Branch on these values, not on message text. New codes may be added over time and are announced in the documentation changelog before they ship; treat any unknown reason as a non-retryable generic rejection.
Quote reference (quoteId attached)
The attached firm quote could not be consumed. Action for all five: request a fresh firm quote and resubmit within its TTL.
reason | When it fires |
|---|---|
quote_not_found | No quote with that ID (or it has already been discarded) |
quote_expired | Quote TTL elapsed before the intent arrived |
quote_already_used | Firm quotes are single-use; this one was already consumed |
quote_pair_mismatch | Quote currency pair differs from the intent's pair |
quote_amount_mismatch | Quote input amount differs from the intent amount (canonical comparison) |
Amounts & bounds
reason | When it fires | Recommended action |
|---|---|---|
below_minimum_output | Computed output (after fees/rate) falls below your minimum | Raise the amount, lower the minimum, or re-quote |
above_maximum_input | Exact-output back-calculation needs more input than your cap | Raise the cap or re-quote |
amount_bounds_range_not_supported | The requested bounds combination is not supported | Fix the request |
input_limit_required_for_exact_output | Exact-output requested without input.limit | Supply the input cap |
Beneficiary
All four are request faults — complete the beneficiary and resubmit.
reason | When it fires |
|---|---|
beneficiary_account_required_for_fiat_output | Fiat output without a beneficiary account |
beneficiary_required_for_remittance | Remittance-class intent without a beneficiary |
beneficiary_incomplete_for_jit_payout | Inline beneficiary missing vendor-required fields (country, payout rail) |
beneficiary_address_required_for_rail | Wire/SWIFT rail requires a postal address (line1, city, state, postalCode) |
Asset & chain
reason | When it fires |
|---|---|
chain_required | On-chain asset without a chain |
unsupported_chain | Chain is not registered on the platform |
unregistered_token | Token address is not in the registry |
invalid_asset_address | Asset address is malformed |
Signatures & permits
reason | When it fires | Recommended action |
|---|---|---|
permit_required | On-chain input without a Permit2 signature | Sign and attach the permit |
erc2612_permit_required | Gasless on-chain input submitted over a non-REST ingress | Gasless intake is REST-only (v1) |
token_permit_unsupported | Token does not support ERC-2612 | Use the pre-approved allowance path |
gasless_exact_output_unsupported | Gasless combined with exact-output | Use exact-input for gasless intents |
Liquidity & solving
Not a request fault — a valid business outcome. Resubmitting later is a new request, not an automatic retry; surface the outcome to the user.
reason | When it fires |
|---|---|
no_liquidity | No liquidity candidates for the corridor |
no_profitable_lp | Quotes exist but no viable execution path |
no_batch_solution | Batch auction produced no solution |
Policy denial is not emitted by the intent endpoint today. When policy checks run at the asynchronous order stage, a denial surfaces as a rejected state on status reads — see Policy subCodes.
The following are reserved for the confirm / cancel flows (coming soon), and are all non-retryable:
INVALID_STRATEGY(400),INTENT_EXPIRED(409),INTENT_ALREADY_EXECUTING(409).
Off-ramp
Errors while converting and confirming the settlement leg (USDC → fiat). Reconciliation detail is internal; failures surface on the intent as a terminal failed status.
| Code | HTTP | Retryable | Recommended action |
|---|---|---|---|
SETTLEMENT_FAILED | 200 intent failed | No | Terminal conversion/confirmation failure (mismatch, short, or timeout) — handle by compensation; do not blind-retry |
PROVIDER_UNAVAILABLE | 5xx | Yes | Dependency (oracle / provider) transiently down — retry with backoff |
Payout
The recipient payout leg moves money, so it is at-most-once: an unconfirmed failure is never resent — the result is re-queried instead. Payout exposes a small, stable set of codes; how they are produced internally is not part of the contract:
| Code | HTTP | Retryable | Recommended action |
|---|---|---|---|
IDEMPOTENCY_KEY_PROCESSING | 409 | Yes (re-query) | Payout in progress / result unconfirmed. Retry with the same Idempotency-Key to re-query — never create a new order or new key |
PAYOUT_UNAVAILABLE | 5xx | Yes | Payout temporarily unavailable — back off and retry |
PAYOUT_REJECTED | 400 | No | Payout rejected — read the message, fix, and resubmit (an accepted payout returns 201) |
Money-safety: on a timeout or dropped connection during payout, do not build a new request. Query the original with the same
Idempotency-Keyand the API returns the settled state safely — this guarantees no double payout. See Idempotency.
Policy subCodes
POLICY_DENIED always carries a machine-readable subCode. It is a closed enum — branch on these values, not on message. New values may be added over time; treat any unknown subCode as non-retryable.
subCode | Meaning | Retryable | Recommended action |
|---|---|---|---|
limits_not_configured | No trading limits are configured for this account | No | Contact onboarding — resending cannot succeed until limits are set |
single_trade_cap_exceeded | The order notional exceeds the per-trade cap | No | Split or reduce the amount (a new request); the cap does not change with time |
currency_limit_exceeded | The currency exposure limit is exhausted | Conditional | See Conditional retries |
compliance_blocked | Blocked by KYC / sanctions / Travel Rule policy | No | Resolve the compliance finding out of band, then submit a new request |
Conditional retries
Most POLICY_DENIED results are terminal, but subCode: "currency_limit_exceeded" is conditional: the same request may succeed once conditions change.
- Signaled as
retryable: false+subCode: "currency_limit_exceeded"— it is not an automatic-backoff case, because resending immediately yields the same denial. - Retry only after (a) the currency limit recovers capacity (other trades settle or roll off), or (b) you reduce the amount.
How retryability is signaled
A dedicated retryable field is planned for error responses. Until it ships, infer the classification from the HTTP status as described above:
| HTTP | Retryable |
|---|---|
5xx | Yes |
429 | Yes |
409 (in progress) | Yes (re-query) |
422 status: "rejected" | No (branch on subCode — see Policy subCodes; currency_limit_exceeded is conditional) |
other 4xx | No |
Once the retryable field is exposed, its value matches the classification on this page.
