Three kinds of failure
| Kind | How you see it | What to do |
|---|---|---|
| Transport | A non-200 status: 401 for a bad or revoked key, 429 when throttled, 5xx if we broke something. | Handle before parsing the body. |
| Request | HTTP 200 with a top-level `errors` array — a malformed query, an unknown field, a missing permission. | A bug in your integration, or a scope you were not granted. Fail loudly. |
| Expected | HTTP 200, no `errors`, and the operation returns a typed result that is not the success type. | A normal outcome. Branch on it. |
Expected failures are values
Many mutations return a union of a success type and one or more error types, rather than throwing. Asking for `__typename` and switching on it is the intended way to use them — an order that cannot move to the state you asked for is a fact about the order, not a fault.
mutation {
transitionOrderToState(id: "1024", state: "Shipped") {
__typename
... on Order { id state }
... on OrderStateTransitionError {
errorCode # e.g. ORDER_STATE_TRANSITION_ERROR
message
fromState
toState
transitionError
}
}
}Permission denials
Calling an operation your key was not scoped for returns a 200 with a permission error in the `errors` array. It is not a bug and retrying will not help — the merchant has to issue a key with the scope you need.
Rate limits
Key traffic is limited twice over a rolling one-minute window: by key, and again by key and source IP together. The per-key ceiling is the higher of the two, so spreading the same key across several machines does not buy you more throughput.
| Limit | Requests per minute |
|---|---|
| Per key | 600 |
| Per key and source IP | 300 |
Only POST requests carrying a key are counted. A merchant clicking around their own dashboard authenticates with a session rather than a key, so their browsing can never eat your quota, and your integration can never slow their dashboard down.
Handling a 429
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json
{
"errorCode": "RATE_LIMITED",
"message": "Too many API requests — please retry shortly."
}- Read Retry-After. It is the number of seconds until the window resets.
- Wait at least that long. Retrying immediately just consumes the next window too.
- Back off exponentially if it happens again, and add jitter — several workers retrying in lockstep re-create the burst that got you throttled.
- Treat a sustained 429 as a design problem, not a transient one: batch your reads, or subscribe to webhooks instead of polling.