Error catalog
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 }
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.
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.
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.
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.
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.
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.
Foutencatalogus
Alle fouten zijn JSON met dezelfde drie velden. Log de hele body: de message is voor mensen geschreven en noemt meestal exact de parameter of het id dat de fout veroorzaakte.
{ "error": "bad_request", "message": "Missing required parameter: from", "status": 400 }
Een parameter ontbreekt, is misvormd of onbruikbaar: een ontbrekende verplichte from-datum, een niet-numeriek id in product_ids, een onbekende vlagwaarde. De API weigert liever dan te gokken; een typfout in product_ids is een 400, nooit een stilletjes vol assortiment.
Reactie: herstel het verzoek. Niet ongewijzigd opnieuw proberen; dezelfde invoer geeft dezelfde 400.
Geen bearer-token, een verlopen token, of een token dat dit account niet mag gebruiken. Access tokens zijn kortlevend — expires_in uit het antwoord dat jouw token gaf is het enige getal om op te vertrouwen.
En ook dit: /api accepteert alleen een access token dat uit een Storekeeper API-sleutel is aangemaakt. Een token dat voor een browsersessie is aangemaakt — wat een merchant krijgt door de app vanuit de backoffice te openen — wordt hier geweigerd, welke rol het ook draagt, en de message zegt dat ook in plaats van je te laten gissen. De oplossing is geen andere rol: maak een sleutel aan op /keys en wissel die in.
Reactie: haal een nieuw token en probeer één keer opnieuw. Wissel je API-sleutel opnieuw in bij het OAuth token-endpoint van je eigen account — die aanroep is goedkoop en herhaalbaar, en er wordt niets door verbruikt. Antwoordt het inwisselen zelf 401, dan is de sleutel dood en niet verlopen: hij is ingetrokken of vervangen, en een mens moet een nieuwe uitgeven. Zie Integratiepatronen.
Het id bestaat niet op dit account (een order, klant, product of locatie), of het pad zelf bestaat niet. Ids zijn per account: een order-id van het ene account resolvet nooit op een ander.
Reactie: behandel het als data, niet als een storing. Had je het id eerder opgeslagen, dan is het record verwijderd of gebruik je het token van het verkeerde account.
Je bent voor deze minuut over de verzoekenlimiet heen. De limiet geldt per API-sleutel — niet per account, niet per IP — dus een koppeling die op hol slaat kan een andere niet afknijpen, mits elke koppeling een eigen sleutel heeft. Eén sleutel over een vloot workers delen betekent één budget samen opmaken, en precies dat moet deze limiet voorkomen.
Schrijfacties zijn apart en strenger begrensd dan leesacties. Een POST, PUT, PATCH of DELETE wordt zowel op de schrijfteller als op de algemene teller afgeschreven, dus een taak die veel leest en weinig schrijft loopt eerst tegen de schrijflimiet aan.
Retry-After bevat het aantal seconden tot het venster opnieuw begint, en de message herhaalt dat. Je hoeft niet geweigerd te worden om te weten waar je staat: elk antwoord op een geauthenticeerde aanroep draagt RateLimit-Limit, RateLimit-Remaining en RateLimit-Reset (seconden tot het venster rolt).
Twee extra headers noemen bij welk plafond die getallen horen. RateLimit-Policy somt elk plafond op dat voor jouw sleutel geldt, elk met een naam, een quotum (q) en een venster in seconden (w). RateLimit geeft de stand van precies één daarvan — het plafond dat je als eerste gaat weigeren — en zegt welk, met r voor wat er over is en t voor de seconden tot het opnieuw begint. Er zijn twee namen, general en write. Bij deze 429 noemt hij het plafond dat weigerde, met r=0, dus je hoeft niet zelf uit te zoeken tegen welke van de twee tellers je aan liep. r en t zijn dezelfde getallen als RateLimit-Remaining en RateLimit-Reset; de naam is wat het paar toevoegt. En omdat RateLimit-Policy uit de configuratie wordt opgebouwd en niet uit je verzoek, draagt ook een gewone GET het schrijfplafond — daar komt een client die alleen leest erachter dat er een strenger plafond op zijn eerste schrijfactie wacht.
Reactie: wacht Retry-After seconden en ga dan verder. Niet heet herhalen — een client die op een 429 blijft rammen verbruikt ook het volgende venster aan weigeringen — en zet er geen extra workers op dezelfde sleutel bij om eromheen te komen. Bepaal je tempo op RateLimit-Remaining in plaats van op een tempo dat je hard hebt gecodeerd: de plafonds zijn deployment-configuratie, en daarom staat hier geen getal en zijn de headers het getal om op te vertrouwen.
Een fout in de API-laag zelf. De response lekt nooit interne details.
Reactie: opnieuw proberen met backoff. Blijft het aanhouden, meld het met tijdstip en pad; wij loggen elk verzoek en kunnen het traceren.
Het Storekeeper-platform achter de API weigerde of faalde de aanroep. De message bevat de upstream-reden wanneer die veilig te delen is.
Reactie: voor leesverzoeken veilig om met backoff opnieuw te proberen. Noemt de message een zakelijke reden (een id in gebruik, een ontbrekende configuratie), herstel die dan in plaats van te herhalen.
Timeouts en verbindingsfouten zonder JSON-body betekenen dat het verzoek wel of niet verwerkt kan zijn. Voor de lees-API is opnieuw proberen dan altijd veilig.