Errors and rate limits

Two things surprise people wiring up a GraphQL API for the first time: a failed operation usually still returns HTTP 200, and the most useful errors are not errors at all but values you are expected to read.

Three kinds of failure

KindHow you see itWhat to do
TransportA non-200 status: 401 for a bad or revoked key, 429 when throttled, 5xx if we broke something.Handle before parsing the body.
RequestHTTP 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.
ExpectedHTTP 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.

graphql
mutation {
  transitionOrderToState(id: "1024", state: "Shipped") {
    __typename
    ... on Order { id state }
    ... on OrderStateTransitionError {
      errorCode        # e.g. ORDER_STATE_TRANSITION_ERROR
      message
      fromState
      toState
      transitionError
    }
  }
}
If you only read the success fields and ignore `__typename`, these come back as nulls and look like empty data. That is the single most common way an integration silently does nothing.

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.

LimitRequests per minute
Per key600
Per key and source IP300

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
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."
}
  1. Read Retry-After. It is the number of seconds until the window resets.
  2. Wait at least that long. Retrying immediately just consumes the next window too.
  3. Back off exponentially if it happens again, and add jitter — several workers retrying in lockstep re-create the burst that got you throttled.
  4. Treat a sustained 429 as a design problem, not a transient one: batch your reads, or subscribe to webhooks instead of polling.
The limits are per key, not per scope, and they are shared by everything that key does. There is no per-scope allowance to spend separately. If a legitimate use case genuinely needs a higher ceiling, ask — do not spread the load over more IP addresses.