Error catalog

Every error, one shape.

All errors are RFC 9457 problem documents, served as application/problem+json. Log the whole body: detail is written for humans and usually names the exact parameter or id that caused it.

{
  "type":     "about:blank",
  "title":    "Bad Request",
  "status":   400,
  "detail":   "Missing required parameter: from",
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736"
}

Branch on type, not on status. type identifies the kind of problem; status restates the HTTP status line and the RFC calls it advisory. about:blank is the RFC's registered value for a problem with no meaning beyond its status code, and then title is that status code's standard phrase — every problem here is about:blank today, so type is not yet a thing to branch on — if one ever earns a type of its own, its card will say so. trace_id is the id this request is filed under, the same value the X-Request-Id response header carries: quote it in a support ticket. A hint member appears on 401 and nowhere else.

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 detail says so rather than leaving you to guess. The fix is not a different role: create a key at /admin/keys and exchange it instead.

This is the only status that carries a hint member: a fixed sentence naming where a bearer comes from. It is deliberately the same on every 401 and says nothing about why this request failed.

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.

There is no special type to match on here. Like every other problem in this catalog this one is about:blank, so the branch that catches being throttled is the 429 status. The machine-readable part of why is in the headers rather than the body: RateLimit below names the ceiling that refused you, and it rides on every authenticated response rather than only on this one. The IETF rate-limit-headers draft that defines those headers also proposes a problem type for this refusal; it is not used, because it is not in IANA's problem-type registry, and an identifier that nine published SDKs branch on must not come from a document that expires.

Retry-After carries the number of seconds until the window resets, and detail 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 — detail is a fixed, generic sentence rather than the underlying exception, and trace_id is what lets us find the one that was yours.

React: retry with backoff. If it persists, report it with the trace_id 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. detail carries the upstream reason when it is safe to share.

React: safe to retry with backoff for reads. If detail 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.