Error Handling
All error responses share a consistent JSON shape:
{
"error": "Human-readable error message",
"code": "MACHINE_READABLE_CODE",
"details": "Optional additional context"
}error— a human-readable string. May change between releases — never branch on it.code— a stable, machine-readable enum. Branch on this. The value set is a contract and will not change for a given meaning (new codes may be added).details— optional extra context for debugging (not user-facing).
On the swap/send/quote endpoints, error bodies also echo back any relevant context fields (inputMint, outputMint, signature, …) — the shape is a strict superset of the table above, so older clients keep working.
HTTP Status Codes
| Code | Meaning | When |
|---|---|---|
200 | Success | Request completed successfully |
202* | Accepted | Reserved for async submission flows |
400 | Bad Request | Invalid request body, missing fields, malformed/oversized/unsigned tx |
401 | Unauthorized | Missing or invalid x-api-key header |
404 | Not Found | No route for the pair, unknown mint, or quote ID not found |
410 | Gone | Quote expired (30s TTL) |
429 | Too Many Requests | Rate limit exceeded — see Retry-After + X-RateLimit-* |
500 | Internal Server Error | Unhandled server-side failure |
502 | Bad Gateway | Submission failed (SEND_FAILED / SUBMISSION_FAILED) |
503 | Service Unavailable | Warming up (blockhash/quote service) — see Retry-After |
504 | Gateway Timeout | Upstream route-validation walk timed out — retryable |
* 202 is not currently emitted; successful submits return 200 with "status": "submitted".
Error Codes
Switch on code rather than the status + prose:
code | Typical status | Meaning | Retry? |
|---|---|---|---|
INVALID_REQUEST | 400 | Malformed body/params (bad JSON, missing field, bad mint) | No — fix the request |
TX_INVALID | 400 | Signed tx bytes could not be deserialized | No |
TX_UNSIGNED | 400 | Transaction has no signature in slot 0 | No |
TX_TOO_LARGE | 400 | Serialized tx exceeds the 1232-byte Solana packet limit | No — rebuild smaller |
QUOTE_NOT_FOUND | 404 | quoteId unknown (never issued or already consumed) | No — re-quote |
QUOTE_EXPIRED | 410 | quoteId valid but its 30s TTL elapsed | Yes — re-quote |
NO_ROUTE | 404 | No routable path for the pair/amount | Maybe — try a different size/pair |
TOKEN_NOT_FOUND | 404 | The mint isn't a token / doesn't exist (/v1/tokens/:mint, search) | No — check the mint |
EXCEEDED_SLIPPAGE | 404 | Candidates exist but all exceed price-impact tolerance | Maybe — raise slippage / smaller size |
SIMULATION_FAILED | 400 | simulate=true and the on-chain simulation reverted | Depends on details |
ROUTE_TIMEOUT | 504 | Upstream route-validation walk timed out | Yes — retry shortly |
BLOCKHASH_UNAVAILABLE | 503 | No recent blockhash yet (warmup) | Yes — retry after Retry-After |
SERVICE_UNAVAILABLE | 503 | Quote/metadata service warming up or unavailable | Yes — retry after Retry-After |
SEND_FAILED | 502 | The node rejected the send — the tx never entered the network | Yes — safe to retry the same signed tx |
SUBMISSION_FAILED | 502 | Transport/internal error mid-submit — landing state unknown | Yes — idempotent; see below |
BUNDLE_UNAVAILABLE | 503 | Bundle submission not configured on this deployment | No |
BUNDLE_REJECTED | 502 | The Jito block engine rejected the bundle | Maybe |
RATE_LIMITED | 429 | Per-key/per-IP rate limit exceeded | Yes — after Retry-After |
INTERNAL | 500 | Unhandled server error | Maybe |
Signature is always echoed on submit failures
On any non-2xx from POST /v1/swap and POST /v1/send (including the 502s above), the response includes the transaction signature. You never need to derive it locally to track a submission whose landing state is unknown:
{ "error": "Submission failed", "code": "SUBMISSION_FAILED", "signature": "5xZt…", "details": "socket hang up" }SEND_FAILED means the tx never entered the network (safe to retry). SUBMISSION_FAILED means the outcome is unknown — poll GET /v1/tx/:sig or retry idempotently (the same signed tx is deduplicated server-side; see Reliability & retries).
Rate Limit Errors
{
"error": "Rate limit exceeded",
"code": "RATE_LIMITED",
"limit": 60,
"tier": "starter",
"retryAfterSeconds": 12
}Every metered response — success and 429 — carries:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Your per-minute quota for this category |
X-RateLimit-Remaining | Requests left in the current window |
X-RateLimit-Reset | Epoch seconds when the window frees up |
Retry-After | (429/503 only) Seconds to wait before retrying |
Read X-RateLimit-Remaining proactively to throttle before you hit 429.
Handling Tips
- Branch on
code, not on the status + prose. - On
429, respectRetry-After; useX-RateLimit-Resetto schedule the next call. - On
503/504, retry afterRetry-After(defaults to 1–2s) — these are warmup/timeout, not hard failures. - On
502, thesignatureis in the body.SEND_FAILED→ retry freely.SUBMISSION_FAILED→ retry idempotently or pollGET /v1/tx/:sig. - See Reliability & retries for the full idempotency + backoff contract.
