Reference

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.codeCause
depth_too_largeA snapshot field depth exceeds its documented maximum.
edge_rate_limitedToo many requests from this IP address at the edge (anti-flood: 20 per second per address, bursts of 40). Retry after 1 second.
history_depthThe requested history goes further back than your plan allows.
https_requiredPlain HTTP is refused: call the same URL over https://. A key sent over http:// travelled unencrypted: rotate it.
insufficient_dataNot enough history to compute the requested indicator.
internal_errorUnexpected server error. A retry with backoff may succeed; if it persists, report it with the response's X-Request-Id header.
invalid_api_keyThe API key is missing, invalid or revoked.
invalid_assetThe asset is not served by this family. The error lists the accepted values.
invalid_channelWebSocket: the channel name is malformed or unknown; the error lists the accepted forms.
invalid_depthA snapshot field depth is not a positive integer.
invalid_indicatorThe POST /v1/indicators body is invalid (id, type or parameters).
invalid_messageWebSocket: the message is not a readable JSON object.
invalid_parameterA parameter has an invalid type or value.
invalid_seriesThe macro series does not exist. See the family's /series catalogue.
invalid_symbolThe symbol is not served. The error lists the accepted values (see /v1/symbols).
invalid_time_rangesince_ms / until_ms are not valid epoch milliseconds, or since_ms > until_ms (until_ms is inclusive), or since_ms >= before_ms.
invalid_timeframeThe timeframe is not accepted by this route.
method_not_allowedThe route exists but not with this HTTP method.
missing_api_keyNo API key was sent: put it in the X-API-KEY header (or api_key in the WebSocket URL).
missing_parameterA required parameter is missing.
not_applicableWebSocket: 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_authenticatedWebSocket: 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_foundUnknown route.
plan_familyYour plan does not include this market data family: the message names it, and GET /v1/limits lists the families of your key.
plan_websocketYour plan does not include the WebSocket stream.
quota_exceededYour 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_limitedYour plan's per-minute rate is exceeded. Retry after Retry-After seconds.
repeated_parameterA parameter appears more than once.
service_unavailableTemporary unavailability. Retry after Retry-After seconds.
timeframe_not_servedThe 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_fieldA ?fields= or snapshot field name does not exist.
unknown_parameterThe route does not declare this parameter.
upstream_timeoutThe request took too long. Reduce the window or depth, then retry.
TypeScript
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

CodeCauseFix
401
API 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.
403
Refused 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.
404
Unknown 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.
405
Method 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.
422
Missing, 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.
429
Rate 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.
503
Momentary 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.
500
Unexpected 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.
504
Request 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

SymptomReal cause
403 while the key is validRead 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 lowThe 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 snapshotA multi-timeframe field asked for without its @tf suffix, or the opposite on a single-form field.
422 on a depth that looks reasonableEvery 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 elsewhereThe intraday macro instruments refuse 30m, and the week only exists on candles and indicators.
An empty response that is not an errorA 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 harmlessEvery 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 rightsince_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.

CaseCode
Invalid timeframe, limit outside 1–1000, unknown symbol422
Empty indicators list, or an entry that is not an object422
Missing id or type key, invalid id, unknown body field422
Unsupported type422
Unknown parameter for this type (obv with a period, for example)422
Parameter of the wrong type, or below its minimum422
macd or adosc with fast greater than or equal to slow422
sar with acceleration greater than maximum422
Duplicate identifier422
Malformed body, more than 50 instances, field too long422
limit plus warm-up exceeds 1000422
Insufficient data for this timeframe, or a volume history too short for a volume indicator422

#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:

CodeReason
4003 · ceilingThe plan’s ceiling on simultaneous connections reached. The reason gives the ceiling. A disconnection frees its slot at once.
4005 · client too slowThe send queue has overflowed ten times. Consume faster, or reduce the number of channels. The reason distinguishes the two cases.
4001Not sent in practice: the key is checked at the handshake (401), and an auth frame on an open session answers authenticated.
4002Session 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, a timeframe that 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 under errors and the response carries partial: 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.

Python
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-After rather 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. A 503 at the handshake only asks you to wait.