Error catalog

Every error, one shape.

All errors are JSON with the same three fields. Log the whole body: the message is written for humans and usually names the exact parameter or id that caused it.

{ "error": "bad_request", "message": "Missing required parameter: from", "status": 400 }
400 Bad Request your input

A parameter is missing, malformed, or unusable: a missing required from date, a non-numeric id in product_ids, an unknown flag value. The API refuses rather than guessing; a typo in product_ids is a 400, never a silently-full catalog.

React: fix the request. Do not retry unchanged; the same input gives the same 400.

401 Unauthorized auth

No bearer token, an expired one, or one this account cannot use. Access tokens are short-lived — expires_in on the response that issued yours is the only number to trust.

Also this: /api accepts only an access token minted from a Storekeeper API key. A token minted for a browser session — what a merchant gets by opening the app from their backoffice — is refused here whatever role it carries, and the message says so rather than leaving you to guess. The fix is not a different role: create a key at /keys and exchange it instead.

React: get a new token and retry once. Exchange your API key again at your own account's OAuth token endpoint — that call is cheap and repeatable, and nothing is consumed by it. If the exchange itself answers 401 the key is dead, not expired: it has been revoked or rolled, and a human has to issue a new one. See Integration patterns.

404 Not Found your input

The id does not exist on this account (an order, customer, product, or location), or the path itself does not exist. Ids are per-account: an order id from one account never resolves on another.

React: treat it as data, not as an outage. If you stored the id earlier, the record was removed or you are calling with the wrong account's token.

429 Too Many Requests rate

You have gone past the request ceiling for the current minute. The ceiling is per API key — not per account, not per IP — so one integration running hot cannot throttle another, provided each has its own key. Sharing one key across a fleet of workers spends one budget between them, which is the thing this limit exists to prevent.

Writes are limited separately and more tightly than reads. A POST, PUT, PATCH or DELETE is charged against both the write counter and the general one, so a job that reads a lot and writes a little meets the write ceiling first.

Retry-After carries the number of seconds until the window resets, and the message repeats it. You do not have to be refused to find out where you stand: every response to an authenticated call carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (seconds until the window rolls).

Two further headers name the ceiling those numbers belong to. RateLimit-Policy lists every ceiling that applies to your key, each one named, with its quota (q) and its window in seconds (w). RateLimit reports the state of exactly one of them — the one closest to refusing you next — and says which, with r for what is left and t for the seconds until it resets. There are two names, general and write. On this 429 it names the ceiling that refused, with r=0, so you never have to work out which of the two counters you ran into. r and t are the same numbers as RateLimit-Remaining and RateLimit-Reset; the name is what the pair adds. And because RateLimit-Policy is built from configuration rather than from your request, a plain GET carries the write ceiling too — which is where a client that only reads finds out a tighter one is waiting for its first write.

React: sleep for Retry-After seconds, then continue. Do not retry hot — a client that hammers a 429 spends the next window on refusals too — and do not add workers on the same key to get around it. Pace yourself off RateLimit-Remaining as you go rather than a rate you hard-coded: the ceilings are deployment configuration, which is why no figure is printed here and why the headers are the number to trust.

500 Internal Server Error ours

A fault in the API layer itself. The response never echoes internals.

React: retry with backoff. If it persists, report it with the timestamp and the path; we log every request and can trace it.

502 Bad Gateway upstream

The Storekeeper platform behind the API rejected or failed the call. The message carries the upstream reason when it is safe to share.

React: safe to retry with backoff for reads. If the message names a business reason (an id in use, a missing configuration), fix that instead of retrying.

Timeouts and connection errors without a JSON body mean the request may or may not have been processed. For the read API that is always safe to retry.