Errors, Rate Limits & Pagination
How the API reports failures, what limits apply, and how list queries page.
Error envelope
Errors come back in a top-level errors array. data is usually null (or the
partially-resolved result). The machine-readable code is at
errors[].extensions.code:
{
"errors": [
{
"message": "Failed to retrieve order",
"path": ["getOrder"],
"extensions": { "code": "INTERNAL_SERVER_ERROR" }
}
],
"data": null
}
HTTP status vs GraphQL errors
- Malformed requests (invalid JSON, GraphQL syntax errors, unknown fields,
missing required variables) return HTTP
400with nodata. - Operation/execution failures (a resolver throwing — not found,
access denied, internal error) return HTTP
200with anerrorsarray.
Branch on errors[].extensions.code, not on the HTTP status.
Error codes you will actually see
The codes below are what the API surfaces today. Several read paths wrap their internal failures in a generic handler, so the granular reason is often collapsed:
extensions.code | When | Message you get |
|---|---|---|
INTERNAL_SERVER_ERROR | Any failure in a read query (getShop, getAllShops, getOrder, getOrders, getProduct, getProducts, getShipment, getShipments) — including not found and cross-tenant access denied | Generic, e.g. "Failed to retrieve order", "Failed to retrieve shop" |
INTERNAL_SERVER_ERROR | Most mutation failures (createShop, updateShop, deleteShop, createProduct, updateProduct, deleteProduct, updateOrder, createOrder) — the human-readable reason is preserved | e.g. "Shop not found", "Unauthorized access to product" |
403 / 404 / 500 | createOrderFromGtin — structured codes are preserved: 403 (shop not found / access denied), 404 (GTIN or product not found), 500 (missing product data) | e.g. "Shop not found or access denied.", "Product not found for GTIN: …" |
createOrderFromGtin surfaces structured codescreateOrderFromGtin now passes through its 403/404/500 codes instead of
masking them as a generic error — so you can branch on extensions.code for the
GTIN order path. Other resolvers still tend to surface
INTERNAL_SERVER_ERROR with a descriptive message; build those branches on
the message string and re-check this table as the mapping is tightened
elsewhere.
Authentication errors
- Invalid / unknown
x-uid→ an error with message"User not found."(codeINTERNAL_SERVER_ERROR). - Missing
x-uid→ the request is unauthenticated; resolvers then fail and return a genericINTERNAL_SERVER_ERROR(e.g."Failed to retrieve ...").
There is currently no dedicated UNAUTHENTICATED / 401 response — a
missing or bad key surfaces as a generic server error, not a clean auth error.
Always send a valid x-uid (see Authentication).
The server currently runs with debug output enabled, so error extensions may
include an exception.stacktrace. Do not parse or depend on stack-trace
contents; rely only on message and extensions.code.
Rate limits
There is no application-level rate limiting enforced by the API today — the server does not impose a per-key request quota. Infrastructure-level limits may still apply upstream. Regardless, be a good citizen:
- Cap concurrency and add exponential backoff with jitter on retries.
- When polling (e.g. tracking), poll on a sensible interval (minutes, not seconds) and back off open orders over time.
- Treat any
INTERNAL_SERVER_ERRORas retryable with backoff; treat HTTP400(malformed query) as a permanent client error — fix the request, don't retry.
Pagination
List queries use limit-only pagination. There are no cursors, no offset,
and no total counts.
| Query | Pagination arg | Behavior |
|---|---|---|
getOrders | limit: Int | Caps the number of orders returned. Omit → returns all matching orders (unbounded). |
getProducts | limit: Int | Caps the number of products returned. Omit → unbounded. |
getBlankProducts | limit: Int | Caps the number of blank variants returned. |
getShipments | limit: Int | Caps the count (applied after a newest-first sort). orderIds narrows to specific owned orders. status and carrier are accepted-but-ignored — see Tracking & Fulfillment. |
Because there is no cursor, you cannot reliably page "the next N" — limit only
truncates the result set. To bound results predictably, scope your queries
(e.g. pass a shopId, or createdAfter / updatedAfter time windows on
getOrders) and request a limit.
query {
getOrders(shopId: "shop_123", createdAfter: { _seconds: 1717200000, _nanoseconds: 0 }, limit: 50) {
id
orderNumber
createdAt { _seconds _nanoseconds }
}
}
When you call getOrders, getProducts, or getShipments without a
shopId, the API gathers your shops (and, for shipments, your orders) and
queries across them. Accounts with a very large number of shops/orders fan out
into multiple batched lookups; if you have many shops, pass an explicit shopId
per call to keep each request lean.