Errors
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/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 | missing_api_key | auth | No x-api-key header |
| 401 | invalid_api_key | auth | Unknown or revoked |
| 429 | rate_limited | auth | Honor retry-after — see 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 | Wallet withdrawal exceeds held_usd |
| 400 | no_login_wallet | account | Verify a login wallet before withdrawing |
| 400 | idempotency_key_required | account | Withdrawals must send the header |
| 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 |
| 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, 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) |
| 400 | credentials_missing | runtime | Attach 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 |
| 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 | |
| — | execution_timeout | runtime | Async: appears as the execution resource's error.code with status: "timed_out", not an HTTP response (executions) |
| — | script_error | runtime | Async: the resource's error.code with status: "failed"; exit_code + stderr tail in details |
| 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
- 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.)
- Codes are append-only. New failure modes add codes; existing codes never change meaning.
- 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. - Creates are retry-safe; money movement demands it.
POST /runtime/deployments,/runtime/backtests, and/runtime/executionsaccept anIdempotency-Keyheader (kept 24h; replays return the original response).POST /wallet/withdrawrequires it. - Destructive overrides are never a query flag. Deleting around open positions takes an explicit body acknowledgement (
force+acknowledge_positions_open) — never a query flag.