Skip to main content
Writing a reliable integration means handling failures well — not just the happy path. This page explains the shape of Customei’s error responses, the HTTP codes you’ll see, and how to behave when you’re rate-limited.

Error shape

GraphQL errors always come back with HTTP 200 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.
{
  "data": null,
  "errors": [
    {
      "message": "You don't have permission to delete this template.",
      "path": ["designTemplateDelete"],
      "extensions": {
        "code": "FORBIDDEN",
        "permission": "template:delete"
      }
    }
  ]
}

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:
const { data, errors } = await response.json();
if (errors) {
  throw new Error(errors[0].message);
}

Partial success

A GraphQL response can carry both data 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:
CodeMeaningWhat to do
200 OKRequest accepted. Check the errors array for application errors.Inspect errors.
400 Bad RequestMalformed request — invalid JSON, missing query, bad variables.Fix the request before retrying.
401 UnauthorizedMissing, invalid, or expired session token.Get a fresh token. See Authentication.
403 ForbiddenAuth worked, but the member lacks the required permission.Grant the permission, or use a different member’s token.
404 Not FoundResource doesn’t exist or isn’t visible to you.Check the ID and the shop/account scope.
413 Payload Too LargeRequest body exceeds the per-request size cap.Split the request into smaller batches.
429 Too Many RequestsYou’re being rate-limited.Back off (see below).
500 Internal Server ErrorUnexpected server error.Safe to retry with backoff.
502 / 503 / 504Upstream or deployment hiccup.Safe to retry with backoff.

Error codes you’ll commonly hit

These come back in errors[].extensions.code. Handle them explicitly in your integration code:
CodeCause
UNAUTHENTICATEDMissing or invalid session token.
FORBIDDENMember lacks the permission for this action. extensions.permission names the missing permission.
NOT_FOUNDReferenced ID doesn’t exist (or is in another account).
VALIDATION_ERRORInput failed schema validation. extensions.fields lists the failing fields.
CONFLICTOptimistic concurrency failure or unique-constraint violation.
RATE_LIMITEDYou hit a per-account rate limit.
INTERNAL_ERRORUnexpected 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:
HeaderMeaning
X-RateLimit-LimitThe total requests allowed in the current window.
X-RateLimit-RemainingHow many you have left.
X-RateLimit-ResetUnix timestamp when the current window resets.
Retry-After(On 429 only) Seconds to wait before retrying.
When you exceed the limit, Customei returns 429 Too Many Requests and a body with errors[0].extensions.code = "RATE_LIMITED".

How to back off

The correct behavior on 429:
  1. Read Retry-After. If present, wait that many seconds. It’s authoritative.
  2. Otherwise, use exponential backoff. Start at 1 second, double on each consecutive 429, cap at 30 seconds.
  3. Add jitter. Random 0–25% on top of the backoff interval prevents retry storms from multiple clients hitting the limit simultaneously.
  4. 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 where filters — 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:
  1. Read before write. Query the current state first, decide if the mutation is still needed, then call it.
  2. Dedupe on retry. If your retry logic might re-issue a createTemplate mutation, check whether the previous attempt already succeeded (by querying for a template with your expected name or external reference) before retrying.
  3. Use upsert style 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 in extensions.permission tells you exactly which override to flip.

Next