---
title: Errors
description: One error shape, a named code for every failure, and a docs link in each.
---

Every error, on every endpoint, is the same shape:

```json
{
  "error": {
    "code": "account_not_funded",
    "message": "The venue account 0x7e15…51f9 holds $0.00; this venue requires at least $5 to start.",
    "docs_url": "https://docs.superior.trade/runtime/credentials",
    "details": null
  }
}
```

- `code` — stable, machine-readable, never reworded. Branch on this.
- `message` — human-readable, specific to the instance (real values, not placeholders).
- `docs_url` — the page that explains the fix. Agents: fetch it as markdown via `/<path>.md`.
- `details` — structured extras where they exist (validation line numbers, coverage windows).

Every response — success or error — also carries an `x-request-id` header. Quote it in any support conversation; it's how we find your exact request in our logs.

## Code registry

| HTTP | Code | Area | Notes |
|---|---|---|---|
| 401 | `unauthorized` | auth | API key or authenticated Privy session is missing, invalid, or not accepted for this route |
| 429 | `rate_limited` | auth | Honor `retry-after` — see [rate limits](/reference/rate-limits) |
| 400 | `email_not_allowed` | account | Disposable/blocked domain |
| 400 | `invalid_otp` | account | Wrong or superseded code |
| 400 | `otp_expired` | account | Code older than 10 minutes |
| 429 | `too_many_attempts` | account | 5 failed tries burns the code |
| 409 | `key_limit_reached` | account | 10 active keys max |
| 400 | `insufficient_held_balance` | account | Arbitrum [wallet withdrawal](/wallet/withdrawals) exceeds `held_usd` |
| 400 | `invalid_withdrawal_source` | account | Send `arbitrum` or `hyperliquid` explicitly |
| 400 | `no_login_wallet` | account | Verify a login wallet before withdrawing |
| 400 | `idempotency_key_required` | account | [Withdrawals](/wallet/withdrawals) must send the header |
| 400 | `withdrawal_otp_required`, `invalid_code` | account | Supply the active challenge and six-digit withdrawal code |
| 409 | `withdrawal_destination_unavailable`, `withdrawal_challenge_mismatch` | account | Register the destination or request a challenge for the current amount |
| 409 | `idempotency_key_conflict`, `withdrawal_state_conflict` | account | Keep the same key and body; never switch to a new key while a withdrawal may be in progress |
| 409 | `withdrawal_source_unavailable` | account | The source is coming soon or unavailable; do not fall back automatically |
| 410 | `code_expired` | account | Request a new withdrawal OTP |
| 429 | `resend_cooldown`, `email_limit_reached`, `attempt_limit_reached` | account | Wait for the applicable OTP limit to reset |
| 502 | `wallet_upstream_unavailable` | account | The managed-wallet provider could not prepare the withdrawal; retry later with the same key |
| 503 | `withdrawal_sponsorship_unavailable` | account | Sponsored Arbitrum submission is unavailable; retain the same key and never send unsponsored |
| 503 | `wallet_repository_unavailable` | account | Retry later with the same key; do not create a replacement key |
| 400 | `unknown_venue` | context | |
| 400 | `unknown_symbol` | context | |
| 404 | `data_unavailable` | context | Covered window in `details` |
| 400 | `window_too_large` | context | Split the request |
| 400 | `unsupported_framework_venue` | runtime | Check the [venue matrix](/context/venues) |
| 400 | `strategy_invalid` | runtime | Framework diagnostics in `details` |
| 400 | `config_invalid` | runtime | Offending key named in `details` |
| 400 | `credential_in_config` | runtime | Secrets go in [credentials](/runtime/credentials), never `config` |
| 409 | `name_taken` | runtime | Deployment `name` is unique per account — a retried create hitting this means the first attempt succeeded |
| 400 | `credential_scope_unsafe` | runtime | Master/withdrawal-capable key rejected by default; override with `allow_unscoped_credential` + acknowledgement fields ([credentials](/runtime/credentials)) |
| 400 | `credentials_missing` | runtime | Attach [credentials](/runtime/credentials) before starting live |
| 400 | `credentials_invalid` | runtime | Venue rejected the key (revoked agent, bad permissions) |
| 402 | `account_not_funded` | runtime | Below the venue's `min_deposit_usd` |
| 409 | `deployment_limit_reached` | runtime | Plan cap — see [usage](/account/usage) |
| 429 | `limit_exceeded` | runtime | Concurrent backtest slots full |
| 409 | `positions_open` | runtime | On delete; force requires body acknowledgement |
| 409 | `already_running` | runtime | |
| 409 | `not_running` | runtime | |
| `422` | `exchange_rejected` | runtime | The venue rejected a typed place or cancel action ([executions](/runtime/executions)) |
| `502` | `venue_unavailable` | runtime | The order venue could not be reached; retry with the same `Idempotency-Key` |
| `503` | `execution_persistence_failed` | runtime | The venue result could not be durably recorded; do not retry with a new `Idempotency-Key` |
| 500 | `internal_error` | server | Our fault — `x-request-id` please |
| 503 | `runtime_unavailable` | server | Control plane can't reach the cluster; **running strategies are unaffected** |

## Design commitments

1. **Distinct problems get distinct codes.** "Not onboarded" is not "not funded"; an agent must be able to pick the right fix without parsing prose. (This is a direct lesson from the current API, where a missing agent wallet surfaced as a funding error.)
2. **Codes are append-only.** New failure modes add codes; existing codes never change meaning.
3. **5xx means us, 4xx means the request.** A venue rejecting your key is a 4xx (`credentials_invalid`) — it's actionable by you. Our cluster being unreachable is a 5xx — retry later, we're paging someone.
4. **Creates are retry-safe; money movement demands it.** `POST /runtime/deployments` and `/runtime/backtests` accept an `Idempotency-Key`; `/runtime/executions` and `/wallet/withdraw` **require** it. Execution replays return the durable completed order record.
5. **Destructive overrides are never a query flag.** Deleting around open positions takes an explicit body acknowledgement (`force` + `acknowledge_positions_open`) — never a query flag.
