Skip to content

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:

ResponseCauseRetryableWhy
5xxServer / infrastructure — transientYesTime resolves it — retry with backoff
422 with status: "rejected"Business rejection — liquidity, constraints, policyNoYou must change the request itself. Some subCodes are conditional — see Policy subCodes
other 4xxClient — request, credentials, or stateNoSame request yields the same result until you change it

Two exceptions4xx that is retryable:

  • 429 (rate limited) — retry after Retry-After with exponential backoff.
  • 409 (in progress) — the original request is still being processed. Retry with the same Idempotency-Key to 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 — return 422 with the original rejection object (status: "rejected" plus a machine-readable reason). 200/202 always 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)

CodeHTTPRetryableRecommended action
INVALID_API_KEY401NoCheck credentials / rotate key
INVALID_PARAMETER, INVALID_PAIR400NoFix the request
IDEMPOTENCY_KEY_REQUIRED, IDEMPOTENCY_KEY_INVALID400NoSupply a valid Idempotency-Key
IDEMPOTENCY_KEY_CONFLICT409NoSame key reused with a different body — use a new key
IDEMPOTENCY_KEY_PROCESSING409Yes (re-query)Retry the same key to fetch the in-flight result
RATE_LIMIT_EXCEEDED429YesBack off, honor Retry-After
SERVICE_UNAVAILABLE, INTERNAL_ERROR5xxYesRetry 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.

reasonWhen it fires
quote_not_foundNo quote with that ID (or it has already been discarded)
quote_expiredQuote TTL elapsed before the intent arrived
quote_already_usedFirm quotes are single-use; this one was already consumed
quote_pair_mismatchQuote currency pair differs from the intent's pair
quote_amount_mismatchQuote input amount differs from the intent amount (canonical comparison)

Amounts & bounds

reasonWhen it firesRecommended action
below_minimum_outputComputed output (after fees/rate) falls below your minimumRaise the amount, lower the minimum, or re-quote
above_maximum_inputExact-output back-calculation needs more input than your capRaise the cap or re-quote
amount_bounds_range_not_supportedThe requested bounds combination is not supportedFix the request
input_limit_required_for_exact_outputExact-output requested without input.limitSupply the input cap

Beneficiary

All four are request faults — complete the beneficiary and resubmit.

reasonWhen it fires
beneficiary_account_required_for_fiat_outputFiat output without a beneficiary account
beneficiary_required_for_remittanceRemittance-class intent without a beneficiary
beneficiary_incomplete_for_jit_payoutInline beneficiary missing vendor-required fields (country, payout rail)
beneficiary_address_required_for_railWire/SWIFT rail requires a postal address (line1, city, state, postalCode)

Asset & chain

reasonWhen it fires
chain_requiredOn-chain asset without a chain
unsupported_chainChain is not registered on the platform
unregistered_tokenToken address is not in the registry
invalid_asset_addressAsset address is malformed

Signatures & permits

reasonWhen it firesRecommended action
permit_requiredOn-chain input without a Permit2 signatureSign and attach the permit
erc2612_permit_requiredGasless on-chain input submitted over a non-REST ingressGasless intake is REST-only (v1)
token_permit_unsupportedToken does not support ERC-2612Use the pre-approved allowance path
gasless_exact_output_unsupportedGasless combined with exact-outputUse 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.

reasonWhen it fires
no_liquidityNo liquidity candidates for the corridor
no_profitable_lpQuotes exist but no viable execution path
no_batch_solutionBatch 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.

CodeHTTPRetryableRecommended action
SETTLEMENT_FAILED200 intent failedNoTerminal conversion/confirmation failure (mismatch, short, or timeout) — handle by compensation; do not blind-retry
PROVIDER_UNAVAILABLE5xxYesDependency (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:

CodeHTTPRetryableRecommended action
IDEMPOTENCY_KEY_PROCESSING409Yes (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_UNAVAILABLE5xxYesPayout temporarily unavailable — back off and retry
PAYOUT_REJECTED400NoPayout 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-Key and 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.

subCodeMeaningRetryableRecommended action
limits_not_configuredNo trading limits are configured for this accountNoContact onboarding — resending cannot succeed until limits are set
single_trade_cap_exceededThe order notional exceeds the per-trade capNoSplit or reduce the amount (a new request); the cap does not change with time
currency_limit_exceededThe currency exposure limit is exhaustedConditionalSee Conditional retries
compliance_blockedBlocked by KYC / sanctions / Travel Rule policyNoResolve 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:

HTTPRetryable
5xxYes
429Yes
409 (in progress)Yes (re-query)
422 status: "rejected"No (branch on subCode — see Policy subCodes; currency_limit_exceeded is conditional)
other 4xxNo

Once the retryable field is exposed, its value matches the classification on this page.