Guide
Errors
Every error on a /v1 path, unknown paths included, uses one JSON body, and the HTTP status is the real status. Branch on the code, not the message.
The error body#
400 invalid_parameter
{
"error": {
"code": "invalid_parameter",
"message": "One or more parameters are invalid.",
"request_id": "0199f0c2-5a1e-7c3b-9d4e-2b6f8a1c3d5e",
"details": [ { "param": "limit", "message": "Must be at most 500." } ]
}
}429 rate_limited
{
"error": {
"code": "rate_limited",
"message": "Rate limit reached: at most 250 requests per minute per account.",
"request_id": "0199f0c2-5a1e-7c3b-9d4e-2b6f8a1c3d5e",
"retry_after_ms": 4200
}
}Error object
errorobjectrequired7 fields insidecodestringrequiredStable machine-readable error code, for example invalid_parameter, rate_limited, data_unavailable.detailsarray<object>2 fields insidemessagestringrequiredparamstringrequired
messagestringrequiredHuman-readable explanation.request_idstringrequiredQuote this when contacting support.retry_after_msintegerMilliseconds to wait before retrying, when the failure is retryable.
retry_after_msand theRetry-Afterheader (whole seconds, rounded up) appear when waiting can help.detailsappears on parameter errors and names every offending parameter.- Messages are written for people and may change. Branch on `code`. Error text never contains internals of the services behind the API.
- The
X-Request-IDresponse header is always present. Quote it when you contact support. - The export endpoints' errors may also carry a
retry_afterfield in seconds, andexport_not_readyhas norequest_idin its body. - The MCP endpoint reports protocol-level failures as JSON-RPC errors and endpoint failures as tool errors holding this body.
Error codes#
| Status | Code | Meaning and what to do |
|---|---|---|
| 400 | invalid_parameter | A value is malformed or out of range. details names the parameters. |
| 400 | unknown_parameter, duplicate_parameter, missing_parameter | See Conventions. |
| 400 | query_too_large | More than 2,048 bytes or 32 parameters in the query. |
| 400 | api_key_in_query | A key was sent in the URL. Use a header. |
| 400 | invalid_cursor | The cursor is invalid, expired, or belongs to a different query or account. |
| 400 | invalid_body | The JSON body of an export request is not valid. |
| 401 | api_key_required, invalid_api_key | No key, or an unknown or revoked key. |
| 401 | invalid_project, account_unavailable | The key's project is inactive or the account is unavailable. |
| 403 | api_subscription_required | The account has no active API subscription. |
| 403 | dataset_release_restricted | The dataset this endpoint belongs to is not enabled for API keys. |
| 403 | overnight_required | Overnight (gth) data needs API Pro. |
| 403 | history_limit_exceeded, history_not_in_plan | The requested times reach back further than your plan's history window. |
| 403 | streaming_not_in_plan | WebSockets need API Pro. |
| 403 | account_suspended | The account is suspended. Contact support. |
| 404 | not_found | Unknown path or resource, or an export that does not exist. |
| 404 | export_not_ready | The export has no file yet. |
| 405 | method_not_allowed | Use GET (POST only on /mcp and /v1/exports). |
| 409 | stream_connection_limit | Your account already has a connection on this WebSocket stream. |
| 413 | range_too_large | The window or result is larger than the endpoint allows. Narrow it. |
| 422 | data_unavailable | The request is valid but there is no data for it: a holiday, an expiration that is not listed, a time before retention begins. |
| 426 | upgrade_required | A WebSocket endpoint was called without an upgrade. |
| 429 | rate_limited, burst_limited | Over the per-minute or per-second limit, or too many concurrent requests. Wait Retry-After. |
| 429 | cooldown | Temporarily blocked after abusive behaviour. Wait Retry-After. |
| 429 | export_limit | Five exports are already queued or running. |
| 500 | internal_error | A fault on our side. Retry, and quote request_id if it persists. |
| 503 | upstream_busy | The data service is at capacity. Retry after Retry-After. |
| 503 | upstream_unavailable | This data is temporarily unavailable, or is not part of the deployment's data entitlements. Retry later. |
| 503 | data_pending | The requested range is being prepared. Retry after Retry-After. This is how a first request for a not-yet-loaded session is answered. |
| 503 | capacity, stream_unavailable, account_state_unavailable | Service capacity, a stream that is not running, or account state that could not be read. Retry after Retry-After. |
| 504 | upstream_timeout | The request exceeded its 20-second budget. Narrow the range or retry. |
Retrying#
Retry 429, 500, 503 and 504 with exponential backoff and jitter, honouring Retry-After when present. Do not retry other 4xx errors unchanged: they will fail the same way. A GET is always safe to retry.
Retry with backoff
async function getJson(url, attempts = 5) {
for (let i = 0; ; i++) {
const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.ORDERMAP_API_KEY}` } });
if (res.ok) return res.json();
const retryable = [429, 500, 503, 504].includes(res.status);
if (!retryable || i === attempts - 1) throw new Error(`${res.status} ${await res.text()}`);
// Honour Retry-After (whole seconds) when present, else back off exponentially with jitter.
const waitMs = Number(res.headers.get("retry-after")) * 1000 || 2 ** i * 500 + Math.random() * 250;
await new Promise((resolve) => setTimeout(resolve, waitMs));
}
}Errors on WebSockets#
Errors before the upgrade are ordinary JSON errors from the table above. After the upgrade a refused request is an error message, and a closed connection carries a close code. Both are listed in WebSockets.