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.
All errors follow the standard error envelope. 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 — return422withstatus: "rejected"and the error envelope.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 with status: "rejected" and a reason — never 200 (see the rule above).
| Code | HTTP | Retryable | Recommended action |
|---|---|---|---|
INSUFFICIENT_LIQUIDITY | 422 rejected | No | No route available — a valid business outcome, not a request fault; surface to the user. Resubmitting later is a new request, not an automatic retry |
CONSTRAINTS_NOT_MET | 422 rejected | No | Output falls below your minimum — adjust amount or minimum (a new request) |
INVALID_PARAMETER | 400 | No | Missing beneficiary / recipient details — complete the request |
AMOUNT_OUT_OF_RANGE | 400 | No | Amount outside the allowed min/max — adjust the amount (a new request) |
INTENT_NOT_FOUND | 404 | No | No intent exists with that ID — check the identifier |
POLICY_DENIED | 422 rejected | No | Blocked by policy (limits / compliance). Carries a machine-readable subCode — see Policy subCodes. When the policy check runs at the asynchronous order stage, the same denial surfaces as a rejected state instead of an HTTP error |
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 the standard response envelope. 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.
