Response codes and errors
HTTP codes
Section titled “HTTP codes”| Code | When it is returned | What to do |
|---|---|---|
200 |
success | — |
201 |
object created | — |
400 |
input validation error | fix the request, do not retry |
401 |
token missing, expired or invalid | refresh the token pair and retry once |
403 |
no permission for the route, company blocked or key not activated | grant the permission; check the company and key status |
404 |
route or object not found | check the path and the identifier |
408 |
waiting for the asynchronous response timed out (600 s; for report export to Excel — 100 s) | check the object state with a separate request |
422 |
business validation error | fix the data |
429 |
the rate limiter triggered on a heavy route | retry with exponential backoff |
500 |
internal error | 1–2 retries, then contact support with the correlation_id |
Body formats
Section titled “Body formats”Classic envelope — this is how most methods and the access check respond
{ "code": 401, "message": "token expired", "error": "token has been expired", "data": null }Error object
{ "error": { "code": "BAD_REQUEST", "message": "shop_id is required" } }Asynchronous response envelope
{ "session_id": "00000000-0000-0000-0000-000000000000", "company_id": "00000000-0000-0000-0000-000000000000", "id": "00000000-0000-0000-0000-000000000000", "status_code": 400, "error": { "code": "BAD_REQUEST", "message": "..." }, "correlation_id": "00000000-0000-0000-0000-000000000000"}In this case the final HTTP code is taken from the status_code field, and the ID of the created
or modified object — from id.
Empty envelope fields are omitted from the response (omitempty); data in an error envelope
is usually absent.
Timeout body — the same channel, but shorter: it has no status_code field.
{ "correlation_id": "...", "error": { "code": "TIMEOUT", "message": "Timeout waiting for response" } }error.code is checked first.
Semantics of key codes
Section titled “Semantics of key codes”401 — token
Section titled “401 — token”The cause is distinguished by the message + error pair:
message |
error |
What happened |
|---|---|---|
token error |
JWT parsing text | the token is truncated, corrupted or signed with a different key |
token expired |
token has been expired |
expired — a refresh is required |
unauthorized |
refresh token used as access |
a refresh token was passed in Authorization |
unauthorized |
session not found / session revoked |
the session is closed or revoked — a new sign-in is required |
unauthorized |
mismatch platform-id with token info |
platform-id differs from the one the token was issued with |
unauthorized |
user is not active / user has been expired |
the key is disabled or has expired |
The token pair is refreshed via POST /v2/auth/refresh (the platform-id header is required).
If the refresh token has also expired, a new token is issued via POST /v1/auth/login.
Errors of the refresh itself come with code 400 and a description of the cause.
403 — permissions
Section titled “403 — permissions”The method exists and the token is valid, but the “method + route template” pair is not in the list allowed for the key.
Two more states respond with the same code: the company is blocked
("error": "company is blocked") and the key is not activated
("error": "user hasn't been activated").
429 — rate
Section titled “429 — rate”Overall limit — 2 requests per second per IP. On top of it, dedicated limiters stand on individual heavy routes: search routes and report exports to Excel (5 requests per second). These counters are shared by all clients of the platform.
408 — asynchronous response timeout
Section titled “408 — asynchronous response timeout”Occurs when the result of an asynchronous operation has not arrived within 600 seconds; for report export to Excel the wait is shorter — 100 seconds. The operation may still have completed — do not retry a write blindly.
Business codes
Section titled “Business codes”String codes
Section titled “String codes”The error / error.code field carries string codes: BAD_REQUEST, TIMEOUT,
TOO_MANY_REQUESTS, rate_limited, and token refresh responses use the RFC 6749 §5.2 codes:
invalid_request, invalid_grant, unauthorized_client, server_error.
Numeric codes — in sales
Section titled “Numeric codes — in sales”Sale methods respond with 400; the exact cause is a number in data.error_code:
{ "status_code": 400, "error": { "code": "", "message": "status: 400 - errors: Bad request - causes: ..." }, "data": { "error_code": 20084, "message": "payment is possible only for calculated order", "data": null }}error.code here is an empty string.
The most common codes in an online store integration:
| Code | Meaning | What to do |
|---|---|---|
20084 |
payment is possible only for a recalculated cart | the items were added with the wrong method — use POST /v2/order-product/:order_id |
20082 |
payment with the shift closed | open a shift in the shop of the sale |
20010 |
the user has no current cash register | open a shift; the system picks the cash register, passing it in the body is useless |
20009 |
the cash register’s shop and the sale’s shop differ | the sale was created in the wrong shop |
20015 |
the sale has no items | add products before payment |
20016 |
insufficient stock | check the stock in the shop of the sale |
20017 |
the draft has been deleted | create a new one |
20020 |
wrong payment amount | the sum of payments must match the receipt total |
20033 |
shift not found | open a shift |
20036 |
product not found | check product_id |
20037 |
the product is archived | an archived product cannot be sold |
20063 |
the sale was not sent to the fiscal data operator (OFD) | removed with the skip_ofd flag |
20085 |
the seller has no access to the shop | check seller_id |
The primary reference is the HTTP code; the numeric and string codes serve for precise diagnostics and logs.
Parsing the error body
Section titled “Parsing the error body”def parse_error(resp): try: body = resp.json() except ValueError: return {"code": resp.status_code, "message": resp.text[:300]} err = body.get("error") if isinstance(err, dict): return {"code": body.get("status_code", resp.status_code), "message": err.get("message") or err.get("code")} return {"code": body.get("code", resp.status_code), "message": body.get("message") or (err if isinstance(err, str) else "unknown")}correlation_id from the response is written to the log — support uses it to find the request
in the trace.