GUIDE
Response handling
One error shape everywhere, stable codes, lists in pages, safe retries and rate limits.
One error shape
Every failure, on every endpoint, carries the same body: a stable code, a short English message for developers, and a requestId. In GraphQL it sits in extensions.
{
"data": null,
"errors": [{
"message": "Sign in again.",
"extensions": { "code": "UNAUTHENTICATED", "message": "Sign in again.", "requestId": "0192f0c1-7a3e-7c11-9f0e-5b2d8c1e4a90" }
}]
}
Branch on code, never on message. Show the user your own text. Quote requestId when you report a problem: it finds every log line for that request. All codes are on the Errors page.
With no data the HTTP status carries the meaning (400, 401, 429, 500). With partial data it is 200 and errors names the failed path.
Lists come in pages
Lists are cursor connections with a totalCount. Ask for first: 20, then pass the last endCursor as after. Page size is 1 to 100; anything else is VALIDATION_FAILED, never silently clamped. Order is stable, and page 500 is as fast as page 1.
Retries and idempotency
Mutations that devices send carry an Idempotency-Key header: a UUID you make once per user action. Retrying with the same key never applies the change twice. After QUERY_TIMEOUT a write may already have committed, so retry a mutation only with the same key.
Limits and rate limits
| Limit | Value | Error |
|---|---|---|
| Depth | 8 levels | QUERY_TOO_COMPLEX |
| Cost | app 1,000; staff 2,500 | QUERY_TOO_COMPLEX |
| Request body | 64 KB | PAYLOAD_TOO_LARGE |
| Time for the database work | 5 s | QUERY_TIMEOUT |
| Requests | per client IP before sign-in, per user after | RATE_LIMITED with Retry-After |
When you hit QUERY_TOO_COMPLEX, ask for smaller pages or fewer nested lists. On RATE_LIMITED, wait Retry-After seconds; do not retry in a tight loop.