Operate
Errors, retries and limits
The error format, idempotency keys, pagination and the rate limit.
The error format
Every failure has the same shape, whatever went wrong:
{
"error": {
"code": "order_not_in_required_state",
"message": "Order is ACCEPTED; this needs it to be PENDING."
}
}code is stable and meant for your program — branch on it. message is for people and may be reworded; log it, show it, never parse it. An invalid_request error also carries details, the problem with each field:
{
"error": {
"code": "invalid_request",
"message": "Some fields are invalid.",
"details": { "items": ["At most 1000 items per push; split larger catalogues."] }
}
}Every response — success or failure — has a Request-Id header (req_…). Include it when you write to us; it finds the exact request in our logs, and in your dashboard's request log.
Status codes and common codes
| Status | Codes you will meet | What to do |
|---|---|---|
| 400 | invalid_request, invalid_connection_code, invalid_timestamp, invalid_cursor, invalid_limit, invalid_webhook_url, fulfilment_unavailable, unknown_barcode |
Fix the request. Retrying unchanged will fail again. |
| 401 | invalid_api_key, authentication_required |
Wrong, revoked or missing key — or a key for the other environment. |
| 403 | forbidden, not_allowed_for_store, key_scope_insufficient |
Your company is not approved for live or is suspended; the store has not allowed this action; or a store-scoped key tried something only a company-wide key may do — claim a code, manage webhook endpoints, retry a delivery. |
| 404 | store_not_found, order_not_found, webhook_endpoint_not_found, sync_not_found |
A store that is not connected to you is a 404 too, deliberately. |
| 409 | order_not_in_required_state, insufficient_stock, request_in_progress, courier_already_booked, not_courier_order |
The thing changed under you. Fetch it again and decide. |
| 422 | idempotency_key_reused |
You sent a different request with an Idempotency-Key already used. A bug on your side. |
| 429 | rate_limited |
Wait for Retry-After seconds. |
| 5xx | — | Our fault. Retry with backoff, using the same Idempotency-Key. |
Idempotency
Networks fail half-way: your request reached us and was carried out, but the answer never reached you. Retry blindly and you might accept an order twice, or apply a stock adjustment twice.
Claiming a store, accepting, rejecting, changing the delivery method, pushing or adjusting stock, acknowledging sales and creating a webhook endpoint all accept an Idempotency-Key header — the reference marks each one. Send a fresh unique value (a UUID) with each logical action, and the same value when you retry it:
curl -X POST https://api.scanimart.com/v1/stores/1042/orders/ORDER-1A2B3C4D5E/accept \
-H "Authorization: Bearer $SCANIMART_KEY" \
-H "Idempotency-Key: 6f1e2a8c-1d9b-4c3e-9a51-2f8e7b6d0c44" \
-H "Content-Type: application/json" \
-d '{"fulfilment_mode": "STORE_STAFF"}'- A retry with the same key and the same body gets the first answer back, with the header
Idempotent-Replayed: true. Nothing happens twice. - The same key with a different body is refused with
422 idempotency_key_reused. - If the first request is still running, a retry gets
409 request_in_progress; wait a moment and retry again. - A
5xxanswer is not remembered, so a retry runs the action for real. - Nor is
403 key_scope_insufficient: it is refused before anything runs, so retry with a company-wide key and the sameIdempotency-Key. - Keys are kept for 24 hours, per environment.
Generate the key when the action is decided — when the cashier presses Accept — and store it with the action, so a retry after your process restarts still uses it.
Pagination
List endpoints return a page and a cursor:
{
"object": "list",
"data": [ "…" ],
"has_more": true,
"next_cursor": "ZTo0MjA5"
}Pass next_cursor back as cursor for the next page, until has_more is false. limit sets the page size, 1–100 (default 50). Cursors are opaque: do not build or edit them.
Timestamps and money
- Timestamps are ISO 8601 with an offset, e.g.
2026-10-08T10:15:00+05:30. Send them the same way; a timestamp without an offset is read as India time. - Amounts are decimal strings in rupees —
"249.00", never a float. Parse them with a decimal type.
Rate limit
Each key may make 600 requests a minute. Over that you get 429 rate_limited with a Retry-After header in seconds. A busy store's traffic — a few orders a minute, a stock push every few minutes — is far below it; if you need more for a bulk job, write to us.
Versioning
This is API version 2026-10-01, shown in every event's api_version. Within a version we only add: new endpoints, new optional request fields, new response fields, new event types. Build your client to ignore fields and event types it does not know. A change that would break a working integration comes as a new version, announced with time to move.
