Error codes
An error always says what was wrong and, when that is possible, what should have been written instead. This page lists the cases, channel by channel, and separates what deserves a retry from what deserves none.
An AI agent connected over MCP receives these same codes: a failed tool call starts with Error (HTTP NNN) and ends with the stable code, so the AI agent can correct its own call. See the MCP server below.
#The shape of an error
Every error has one shape, whatever the layer that refused (gateway, service or validation): status: "error", server_time and an error object with a stable code, an English message and, when one parameter is at fault, param. error.details, an object present when the error carries structured data, gives the same things for code: suggestion, accepted, your plan's window on a history_depth, retry_after_s on time-based refusals. Nothing sits next to error.
{
"status": "error",
"server_time": "2026-09-30T16:07:27.808Z",
"error": {
"code": "invalid_symbol",
"message": "Unknown symbol 'BTCUSD'. Did you mean 'BTCUSDT'? Accepted values: ADAUSDT, AVAXUSDT, BNBUSDT, BTCUSDT, DOGEUSDT, ETHUSDT, GRAMUSDT, HBARUSDT, HYPEUSDT, LINKUSDT, LTCUSDT, NEARUSDT, SOLUSDT, SUIUSDT, TAOUSDT, TRXUSDT, UNIUSDT, USDCUSDT, USDTUSDC, XLMUSDT, XMRUSDT, XRPUSDT.",
"param": "symbol",
"details": {
"suggestion": "BTCUSDT",
"accepted": [
"ADAUSDT",
"AVAXUSDT",
"BNBUSDT",
"BTCUSDT",
"DOGEUSDT",
"ETHUSDT",
"GRAMUSDT",
"HBARUSDT",
"HYPEUSDT",
"LINKUSDT",
"LTCUSDT",
"NEARUSDT",
"SOLUSDT",
"SUIUSDT",
"TAOUSDT",
"TRXUSDT",
"UNIUSDT",
"USDCUSDT",
"USDTUSDC",
"XLMUSDT",
"XMRUSDT",
"XRPUSDT"
]
}
}
}Refusals tied to the plan have the same shape, plus error.details.retry_after_s on time-based refusals (also in the Retry-After header). The X-Deny-Reason header names the reason: rate_limited, quota_exceeded, plan_family, plan_websocket, missing_api_key, invalid_api_key or history_depth.
{
"status": "error",
"server_time": "2026-09-30T16:07:27.808Z",
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded: your plan allows a limited number of requests per minute. Retry after the Retry-After delay.",
"details": {
"retry_after_s": 2
}
}
}The depth 403 carries X-Deny-Reason: history_depth and error.code: history_depth, like the other refusals tied to the plan; its message gives the maximum usable limit on that timeframe. The windows per plan are on the Usage limits page.
The error codes
Branch on error.code, never on the text of the message: the code is a contract, the message may be reworded. Adding a code is not a breaking change; renaming or removing one is.
| error.code | Cause |
|---|---|
depth_too_large | A snapshot field depth exceeds its documented maximum. |
edge_rate_limited | Too many requests from this IP address at the edge (anti-flood: 20 per second per address, bursts of 40). Retry after 1 second. |
history_depth | The requested history goes further back than your plan allows. |
https_required | Plain HTTP is refused: call the same URL over https://. A key sent over http:// travelled unencrypted: rotate it. |
insufficient_data | Not enough history to compute the requested indicator. |
internal_error | Unexpected server error. A retry with backoff may succeed; if it persists, report it with the response's X-Request-Id header. |
invalid_api_key | The API key is missing, invalid or revoked. |
invalid_asset | The asset is not served by this family. The error lists the accepted values. |
invalid_channel | WebSocket: the channel name is malformed or unknown; the error lists the accepted forms. |
invalid_depth | A snapshot field depth is not a positive integer. |
invalid_indicator | The POST /v1/indicators body is invalid (id, type or parameters). |
invalid_message | WebSocket: the message is not a readable JSON object. |
invalid_parameter | A parameter has an invalid type or value. |
invalid_series | The macro series does not exist. See the family's /series catalogue. |
invalid_symbol | The symbol is not served. The error lists the accepted values (see /v1/symbols). |
invalid_time_range | since_ms / until_ms are not valid epoch milliseconds, or since_ms > until_ms (until_ms is inclusive), or since_ms >= before_ms. |
invalid_timeframe | The timeframe is not accepted by this route. |
method_not_allowed | The route exists but not with this HTTP method. |
missing_api_key | No API key was sent: put it in the X-API-KEY header (or api_key in the WebSocket URL). |
missing_parameter | A required parameter is missing. |
not_applicable | WebSocket: the channel has no meaning for this asset (a futures channel of a stablecoin). The REST API answers such a request with 200 and unavailable: not_applicable. |
not_authenticated | WebSocket: a message other than auth arrived on a session not yet authenticated (rare: the key is checked at the handshake, where a missing key answers 401 invalid_api_key). |
not_found | Unknown route. |
plan_family | Your plan does not include this market data family: the message names it, and GET /v1/limits lists the families of your key. |
plan_websocket | Your plan does not include the WebSocket stream. |
quota_exceeded | Your plan's monthly quota is exhausted. Retry-After (and error.details.retry_after_s) give the seconds until it resets, on the first day of the month (UTC). |
rate_limited | Your plan's per-minute rate is exceeded. Retry after Retry-After seconds. |
repeated_parameter | A parameter appears more than once. |
service_unavailable | Temporary unavailability. Retry after Retry-After seconds. |
timeframe_not_served | The series exists but does not serve this timeframe (a daily-only series asked in 1h). A timeframe the route never serves is invalid_timeframe. |
unknown_field | A ?fields= or snapshot field name does not exist. |
unknown_parameter | The route does not declare this parameter. |
upstream_timeout | The request took too long. Reduce the window or depth, then retry. |
const response = await fetch(url, { headers: { "X-API-KEY": key } });
const body = await response.json();
if (body.status === "error") {
switch (body.error.code) {
case "rate_limited":
case "service_unavailable":
// Transient: wait Retry-After seconds, then retry.
break;
case "history_depth":
// Ask for less history, or upgrade: error.message gives the max limit.
break;
case "invalid_symbol":
case "unknown_parameter":
// Fix the request: error.param names the culprit, error.message the fix.
break;
default:
throw new Error(body.error.code + ": " + body.error.message);
}
}#HTTP codes
| Code | Cause | Fix |
|---|---|---|
401API key refused | The API key is missing, invalid or revoked (error.code: invalid_api_key). The response carries WWW-Authenticate: ApiKey header="X-API-KEY". | Check that the X-API-KEY header really is sent: some HTTP clients drop it after a redirect. Create or copy a key from the console. |
403Refused by the plan | The key is valid but the plan does not cover the request; the X-Deny-Reason header carries the same code as error.code: plan_family (market data family outside the plan), plan_websocket (WebSocket stream outside the plan) or history_depth (a history depth beyond the plan’s window). | On plan_family and plan_websocket, the data exists but is not in your plan. On history_depth, error.message gives the maximum limit to use. |
404Unknown route | The path does not exist (error.code: not_found). The message suggests the closest route when there is one. | Check the path: the message gives the closest route when there is one; every route is in the contract. |
405Method not allowed | The route exists but not with this method (error.code: method_not_allowed); the Allow header lists the accepted ones. | Use GET (POST only for /v1/indicators); HEAD is accepted on every GET route and OPTIONS (the CORS preflight) on every route. |
422Missing, unknown or invalid parameter | An unknown or disabled symbol, asset or series, a depth beyond the field’s cap, an indicator parameter out of bounds, symbol missing, timeframe outside the list, multi-timeframe field requested without the @tf suffix, unknown field in ?fields=, parameter the route does not declare or parameter repeated, since_ms or until_ms given in seconds or out of range, invalid JSON body. | On an unknown field or parameter, the response suggests the closest name. On a timeframe, it lists the accepted values. |
429Rate or quota exceeded | Either more requests per minute than your plan allows, or too dense a burst (X-Deny-Reason: rate_limited, Retry-After: the wait for the next token); or the account’s monthly quota is exhausted (X-Deny-Reason: quota_exceeded, Retry-After: the seconds until it resets). | On rate_limited, wait the seconds given by Retry-After. On quota_exceeded, the counter restarts on the first day of the month (UTC); you can also change plan from the console. |
503Momentary unavailability | A read dependency is recovering. At the WebSocket handshake, the stream service has reached its capacity, for all accounts together: this is not your plan’s connection ceiling, which closes with 4003. | Retry after the delay carried by Retry-After, or with an increasing delay when it is absent. The error is transient by construction. |
500Unexpected error | A fault on the service side, never caused by the request. | Retry; if the response persists, report it with the X-Request-Id header of the response. |
504Request too long | The query took too long for the window or depth asked (error.code: upstream_timeout). | Reduce limit or the time range (since_ms / until_ms), then retry: replaying the same request takes the same time. |
The most frequent confusions
| Symptom | Real cause |
|---|---|
| 403 while the key is valid | Read X-Deny-Reason (the error.code). plan_family: the family of this endpoint is not in your plan. plan_websocket: the stream is not in it. history_depth: the request goes back beyond the window of the plan, and error.details gives the largest limit. invalid_api_key with no apparent reason: an HTTP client that drops custom headers after a redirect; call the final URL. |
| 429 while the pace is low | The monthly quota of the account is exhausted (X-Deny-Reason: quota_exceeded, Retry-After: seconds until it resets), not the rate limit. It restarts on the first day of the month, in UTC, or as soon as the plan changes. |
| 422 with no clear message on a snapshot | A multi-timeframe field asked for without its @tf suffix, or the opposite on a single-form field. |
| 422 on a depth that looks reasonable | Every snapshot field has its own ceiling, in its own unit. The CVD stops at 200 windows, the inter-venue spreads at 50. |
| 422 on a timeframe that is valid elsewhere | The intraday macro instruments refuse 30m, and the week only exists on candles and indicators. |
| An empty response that is not an error | A stablecoin on a futures metric: the unavailable key says which of the three reasons applies. (Options and ETF routes serve BTC and ETH only: another asset is a 422 invalid_asset that lists them.) |
| 422 on a parameter that looks harmless | Every route refuses a parameter it does not declare, and a parameter passed twice; the snapshot does the same with an unknown key. The response suggests the closest name: a misspelt parameter is never silently ignored. |
| 422 on a since_ms that looks right | since_ms and until_ms are in milliseconds. A timestamp in seconds, in microseconds or in nanoseconds is refused before anything runs, as is a bound negative, inverted or earlier than 2009-01-03 — except since_ms=0, which means the start of the data. |
#Indicators
Validation follows a fixed order (timeframe, limit, symbol, parsing of the indicators, ceiling rule, reading of the candles), and the first failure wins. A corrected body can therefore reveal a second error.
| Case | Code |
|---|---|
| Invalid timeframe, limit outside 1–1000, unknown symbol | 422 |
| Empty indicators list, or an entry that is not an object | 422 |
| Missing id or type key, invalid id, unknown body field | 422 |
| Unsupported type | 422 |
| Unknown parameter for this type (obv with a period, for example) | 422 |
| Parameter of the wrong type, or below its minimum | 422 |
| macd or adosc with fast greater than or equal to slow | 422 |
| sar with acceleration greater than maximum | 422 |
| Duplicate identifier | 422 |
| Malformed body, more than 50 instances, field too long | 422 |
| limit plus warm-up exceeds 1000 | 422 |
| Insufficient data for this timeframe, or a volume history too short for a volume indicator | 422 |
#WebSocket stream
Every impossible request receives an op: error message that says what was expected: the list of valid symbols, that of the channels, the bounds of depth. Three situations produce not an error but a closure:
| Code | Reason |
|---|---|
| 4003 · ceiling | The plan’s ceiling on simultaneous connections reached. The reason gives the ceiling. A disconnection frees its slot at once. |
| 4005 · client too slow | The send queue has overflowed ten times. Consume faster, or reduce the number of channels. The reason distinguishes the two cases. |
| 4001 | Not sent in practice: the key is checked at the handshake (401), and an auth frame on an open session answers authenticated. |
| 4002 | Session never authenticated within five seconds. A residual case, since the key is checked at the handshake. |
The key is checked at the handshake, before the opening: a missing key receives a 401 missing_api_key, an unknown or revoked one a 401 invalid_api_key (with WWW-Authenticate), a plan without the stream (Free) a 403 plan_websocket — the value of X-Deny-Reason each time. A 429 there signals too high an opening rate, and a 503 a stream service that has reached its capacity, all accounts together: retry a little later. Once connected, a channel whose family is not in the plan receives op: error with error.code: plan_family and the whole subscription is refused. See Usage limits.
#MCP server
The errors follow a single pattern, Error (HTTP NNN): message [code: …], and a failed tool call is flagged as an MCP error (isError), with the same error in structuredContent.error: status, code, message, and param, hint, retry_after_s when they apply. Every tool call is judged as a REST request with the client’s key: a 403 for a family outside the plan or a 429 for quota come back as they are, with their message and their error.code. Two families are specific to it:
- Invalid request: an unknown field, a missing target (
symbol,asset,series) or one that no requested field expects, a parameter the chosen route does not accept, atimeframethat no field uses, a dated window on a field that has none. Everything is listed in a single message, before any network call. - Partial response: in the five MCP data tools (
get_pair_data,get_options_data…), a field in error does not block the others. It is reported undererrorsand the response carriespartial: true; the call itself only fails when no field succeeds.
A retry only happens when the upstream did not answer (502, 503, 504 or a network failure): a 500 is not replayed, because every request counts against the client’s quota. A timeout returns Error (HTTP 504) after thirty seconds.
#Retry strategy
Not everything can be replayed. Replaying a faulty request produces exactly the same response, and consumes quota for nothing.
import httpx # pip install httpx
def should_retry(response: httpx.Response) -> bool:
"""What is transient, and what never will be (the official SDKs do the same)."""
# A spent monthly quota only resets on the 1st of the month: fail fast.
if response.status_code == 429 and response.headers.get("X-Deny-Reason") == "quota_exceeded":
return False
# 429 (rate) and 503: the server says when, in Retry-After. 502 and 504:
# a gateway hiccup or a slow query. Retry with an increasing delay.
if response.status_code in (429, 502, 503, 504):
return True
# 500: a fault on the service side, retry once.
if response.status_code == 500:
return True
# 401, 403, 404, 405, 422: the request is at fault. Replaying it as is
# produces exactly the same response.
return False- Respect
Retry-Afterrather than inventing a delay: the server knows when it will be ready. - Bound the number of attempts. Three or four are enough: beyond that, the problem is not transient.
- Do not replay a 4xx, 429 aside. Correct the request.
- On the stream, reconnect with an increasing delay, and read the close code:
4003, the ceiling of the plan is reached, another connection of the account has to close first;4005(client too slow), the client is overloaded, and reconnecting without changing the consumption will reproduce the cut. A503at the handshake only asks you to wait.