Skip to content

Errors

Every error, from any route including unknown ones, has the same JSON body:

{"error": {"code": "bad_request", "message": "hs6 must be exactly 6 digits"}}

code is a stable machine-readable string; message is for humans and may change. Branch on the HTTP status or on code, never on the message text. The X-Request-Id header is present on errors too; quote it when you report a problem.

Codes

code HTTP When Retry?
bad_request 400 A parameter is missing, malformed or out of range No. Fix the request.
unauthorized 401 Key missing, malformed, wrong or revoked. Response has WWW-Authenticate: Bearer. No. Fix or replace the key.
forbidden 403 The key is valid but lacks the data scope No. Ask for a key with the right scope.
not_found 404 Unknown route, or the entity does not exist (for example a well-formed but unknown company INN) No.
method_not_allowed 405 Wrong HTTP method. REST routes accept GET only; /mcp accepts POST only. The Allow header names the right method. No.
rate_limited 429 The key's request budget is used up. Response has Retry-After (whole seconds). Yes, after Retry-After seconds.
timeout 504 The query ran longer than the service's 30 second limit Once or twice, then narrow the request.
internal 500 The service failed (for example the database is unavailable) Yes, with backoff.

GET /v1/health is the one place a 503 appears: {"data": {"ok": false, "db": false}, ...} when the database check fails.

Examples

400 bad_request

Real responses:

GET /v1/products/38244
{"error": {"code": "bad_request", "message": "hs6 must be exactly 6 digits"}}
GET /v1/calendar?days=abc
{"error": {"code": "bad_request", "message": "invalid or missing parameter(s): days"}}

The second form lists every offending parameter, comma-separated. Other messages you can get from bad_request (all observed from the service): start/end must be YYYY-MM-DD; resample must be '', week, month, quarter or year; years must be between 1 and 20; give at least one filter (query, inn, okved_prefix, region, ...) for company search without any filter; query is too short or only a legal-form word ... after normalisation; add a more distinctive word ... when the only filter is a query that normalises to nothing; inn must be 10 (legal entity) or 12 (individual) digits for a malformed INN; hs must have at least 4 digits ... for a too-short HS code on /v1/restrictions/{hs}; a parameter value was rejected by the database for a value that fits the type but not the data.

Do not retry a 400 unchanged: it will fail the same way.

401 unauthorized

Real response, same body for a missing key and a wrong key:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer
{"error": {"code": "unauthorized", "message": "missing or invalid API key"}}

See Authentication.

403 forbidden

The key authenticated but has no data scope. This is what the service sends:

{"error": {"code": "forbidden", "message": "this key lacks the 'data' scope"}}

404 not_found

Real responses:

GET /v1/companies/0000000000
{"error": {"code": "not_found", "message": "no such company"}}
GET /v1/nothing
{"error": {"code": "not_found", "message": "no such route"}}

Note

Not every empty result is a 404. A series id that does not exist returns 200 with an empty rows list, and /v1/latest leaves unknown ids out of rows and lists them in data.missing. Only entity lookups by key (a well-formed but unknown company INN) and unknown routes return 404; a malformed INN is a 400. See the FAQ.

405 method_not_allowed

A POST to a REST route, or a GET to /mcp:

{"error": {"code": "method_not_allowed", "message": "method not allowed"}}

The /mcp endpoint answers GET and DELETE with the message use POST and the header Allow: POST. REST routes answer with Allow: GET.

429 rate_limited

HTTP/1.1 429 Too Many Requests
Retry-After: 3
{"error": {"code": "rate_limited", "message": "rate limit exceeded"}}

Retry-After is the number of whole seconds (at least 1) until one request is available again. Wait that long, then retry. Details and a backoff helper: Rate limits.

504 timeout and 500 internal

{"error": {"code": "timeout", "message": "query exceeded the statement timeout"}}
{"error": {"code": "internal", "message": "database unavailable"}}

A 500 can also carry the message internal error for an unexpected failure. Error messages never contain SQL, connection details or server paths.

A 504 means the request asked for too much work. Retrying the identical request tends to time out again, so retry at most once or twice and then make the request smaller: a shorter date window, resample, a tighter filter. A 500 is the service's problem, not yours: retry with backoff, and if it persists, report it with the X-Request-Id.

Retry guidance

Status Retry How
400, 401, 403, 404, 405 No The same request will fail the same way. Fix the request or the key.
429 Yes Sleep for Retry-After seconds, then retry. Do not retry immediately.
500, 504 Yes, limited Exponential backoff (1 s, 2 s, 4 s ...) with jitter, three to five attempts at most. For 504, shrink the request.
Network errors, connection resets Yes, limited Same as 500. All REST calls are GET and safe to repeat.

A minimal Python implementation of this table is in Rate limits.

Errors over MCP

On /mcp, authentication, scope, rate-limit and method errors are plain HTTP responses with the bodies above. Errors inside a tool call (bad arguments, timeouts) come back as a normal MCP result whose text is {"error": {"code": ..., "message": ...}}, with the same code values.