Errors
One error envelope for every failure, ten error codes, and what to do about each one.
The Error Envelope
Every failure, whatever the endpoint, answers with an HTTP status of 400 or above and this body:
{
"status": {
"timestamp": "2026-10-11T14:00:00.000Z",
"elapsed_ms": 2,
"error_code": 1003,
"error_message": "Market not found",
"request_id": "req_7Hk2pQ9xLm"
},
"error": {
"code": "not_found",
"message": "Market not found",
"retryable": false
}
}| Field | Type | Meaning |
|---|---|---|
error.code | string | The machine-readable code. Branch on this. |
error.message | string | A short human-readable explanation. Its wording can change; do not parse it. |
error.retryable | boolean | True when the same request can succeed later unchanged. |
error.retry_after_ms | integer, optional | The exact wait before retrying, on rate and stream-quota errors. |
status.error_code | integer | The numeric form of error.code (1000 to 1009), 0 on success. |
status.error_message | string | The same text as error.message. |
status.request_id | string | Identifies this request in our logs. Quote it when you write to us. |
The contract also allows an optional error.details object; the API does not send one today, and a validation error does not name the parameter that failed.
Error Codes
invalid_requestHTTP 400error_code 1000not retryableA parameter failed validation (a wrong type, a value outside its bounds, a malformed ID), the cursor does not belong to this query, or time_start is not before time_end. The message does not name the parameter.
Fix: Check every parameter against the reference: bounds, enums, comma-separated limits and the ID prefixes. Restart pagination without the cursor if the query changed.
unauthenticatedHTTP 401error_code 1001not retryableThe X-API-Key header is missing, or the key is not one the API knows.
Fix: Send the key in the X-API-Key header, exactly as issued. A key that ended more than 30 days ago is removed and answers 401.
forbiddenHTTP 403error_code 1002not retryableThe key is known but its plan is not active: it expired, was suspended, or has not started yet.
Fix: Check the key's state under Account, API keys on cantoncap.cc, renew the plan, or write to api@cantoncap.cc.
not_foundHTTP 404error_code 1003not retryableThe resource does not exist or is not published, the path is unknown, or there is nothing to return (for example no current order book, or no quote in a chart range).
Fix: Resolve IDs with GET /v1/assets/map or the list endpoints. A 404 never says whether an unpublished record exists.
unsupported_capabilityHTTP 422error_code 1004not retryableThe market or source cannot answer this operation (an order book for an AMM market), or the request is larger than the operation supports.
Fix: Check the market model or the venue's capabilities first; narrow the range or the ID list.
rate_limitedHTTP 429error_code 1005retryableThe key used up its requests for the current one-minute window.
Fix: Wait for Retry-After seconds (or error.retry_after_ms), then retry. Spread requests across the window.
quota_exceededHTTP 403error_code 1006not retryableThe request asks for more than the plan allows: a limit above the plan's page size, or a range reaching further back than its history depth.
Fix: Lower limit, or move time_start forward. A larger plan raises both bounds.
conflictHTTP 409error_code 1007not retryableRaised by administrative write operations only. Public read endpoints do not return it.
Fix: Not applicable to the public API.
internal_errorHTTP 500error_code 1009not retryableAn unexpected failure on the API side.
Fix: Retry once later; if it persists, write to tech@cantoncap.cc with the request_id.
Each endpoint in the reference lists the errors it can answer with and the exact messages.
Handle Errors in Code
const response = await fetch(url, { headers: { "X-API-Key": key } });
const body = await response.json();
if (!response.ok) {
const { code, message, retryable, retry_after_ms } = body.error;
switch (code) {
case "rate_limited":
case "source_unavailable":
// Retryable: wait retry_after_ms when given, else back off exponentially.
break;
case "unauthenticated":
case "forbidden":
// The key is missing, wrong or not active. Alert a human; do not retry.
break;
case "quota_exceeded":
// Ask for less: a smaller limit or a shorter range.
break;
case "not_found":
// The record is unknown or unpublished. Treat as absent.
break;
default:
// invalid_request, unsupported_capability, internal_error
throw new Error(`${code}: ${message} (${body.status.request_id})`);
}
}A full retry loop with backoff is on Rate limits.
/v1/simple/price leave out IDs they cannot answer for instead of failing, and a currency without an observation is left out of quotes. Check for the key before you read it.Getting Help
If an error persists and you cannot explain it, write to tech@cantoncap.cc with the request_id, the time, the endpoint and the parameters (never the key). See Support and legal.