Error shape
GraphQL errors always come back with HTTP200 OK and an errors array in the response body — that’s a GraphQL convention, not a Customei quirk. Each error has a message, a path into the query, and optional extensions carrying structured metadata.
Always inspect errors
Because GraphQL returns HTTP 200 for application-level errors, code that only checks response.ok will silently swallow them. Every GraphQL client worth using exposes a way to surface them:
Partial success
A GraphQL response can carry bothdata and errors at the same time — for example, a query that asked for two fields where one resolved and one didn’t. Treat this as a partial failure: log the error, decide whether to fall back on the partial data, and don’t assume data is clean just because it’s present.
HTTP status codes
Transport-level failures — authentication, rate limiting, the server being down — use standard HTTP codes:| Code | Meaning | What to do |
|---|---|---|
200 OK | Request accepted. Check the errors array for application errors. | Inspect errors. |
400 Bad Request | Malformed request — invalid JSON, missing query, bad variables. | Fix the request before retrying. |
401 Unauthorized | Missing, invalid, or expired session token. | Get a fresh token. See Authentication. |
403 Forbidden | Auth worked, but the member lacks the required permission. | Grant the permission, or use a different member’s token. |
404 Not Found | Resource doesn’t exist or isn’t visible to you. | Check the ID and the shop/account scope. |
413 Payload Too Large | Request body exceeds the per-request size cap. | Split the request into smaller batches. |
429 Too Many Requests | You’re being rate-limited. | Back off (see below). |
500 Internal Server Error | Unexpected server error. | Safe to retry with backoff. |
502 / 503 / 504 | Upstream or deployment hiccup. | Safe to retry with backoff. |
Error codes you’ll commonly hit
These come back inerrors[].extensions.code. Handle them explicitly in your integration code:
| Code | Cause |
|---|---|
UNAUTHENTICATED | Missing or invalid session token. |
FORBIDDEN | Member lacks the permission for this action. extensions.permission names the missing permission. |
NOT_FOUND | Referenced ID doesn’t exist (or is in another account). |
VALIDATION_ERROR | Input failed schema validation. extensions.fields lists the failing fields. |
CONFLICT | Optimistic concurrency failure or unique-constraint violation. |
RATE_LIMITED | You hit a per-account rate limit. |
INTERNAL_ERROR | Unexpected server error. Safe to retry. |
Rate limits
Customei enforces per-account rate limits across the API. Limits are shared between all members of an account, so a noisy automation can impact interactive admin users on the same account — budget accordingly.How limits are signaled
When you’re near or over a limit, responses carry these headers:| Header | Meaning |
|---|---|
X-RateLimit-Limit | The total requests allowed in the current window. |
X-RateLimit-Remaining | How many you have left. |
X-RateLimit-Reset | Unix timestamp when the current window resets. |
Retry-After | (On 429 only) Seconds to wait before retrying. |
429 Too Many Requests and a body with errors[0].extensions.code = "RATE_LIMITED".
How to back off
The correct behavior on429:
- Read
Retry-After. If present, wait that many seconds. It’s authoritative. - Otherwise, use exponential backoff. Start at 1 second, double on each consecutive 429, cap at 30 seconds.
- Add jitter. Random 0–25% on top of the backoff interval prevents retry storms from multiple clients hitting the limit simultaneously.
- Give up after N attempts. 5 is usually the right cap for an automation; beyond that you’re almost certainly not going to succeed without human intervention.
Tips for staying under the limit
- Batch reads — GraphQL lets you fetch many resources in one round trip. One query asking for 50 templates costs less than 50 individual queries for one template each.
- Use
wherefilters — don’t fetch everything and filter client-side. - Cache what doesn’t change — templates, option sets, and libraries change infrequently; re-reading them on every hit is wasteful.
- Prefer webhooks for change detection — polling the API for “is this order paid yet?” is the most common way to burn through a rate limit. Webhooks push the event to you instead.
Idempotency for mutations
Customei doesn’t currently require an idempotency key on mutations, but you should structure your code to be idempotent on retry. The safest pattern:- Read before write. Query the current state first, decide if the mutation is still needed, then call it.
- Dedupe on retry. If your retry logic might re-issue a
createTemplatemutation, check whether the previous attempt already succeeded (by querying for a template with your expected name or external reference) before retrying. - Use
upsertstyle mutations where available — they’re naturally idempotent.
Debugging a failed request
When something fails and you can’t immediately tell why:- Log the full response body, not just the status code. GraphQL puts the real reason in
errors[].message. - Check
extensions.code— it’s more stable than the message. - Check your session token. Stale tokens are the single most common cause of mysterious
401s. - Reproduce in GraphiQL. If it works in the playground but fails from your code, the bug is in your request construction, not in the server.
- Check the member’s permissions.
403+ a permission code inextensions.permissiontells you exactly which override to flip.
Next
- Authentication
- GraphQL quickstart
- Webhooks — for push-based updates instead of polling.