Responses and errors
The success and error envelopes, every error code with its HTTP status and what to do, the shape of validation errors, and which errors to retry.
Every response from the API has the same outer shape, and every error has a stable code you can branch on. Branch on error.code, not on the HTTP status or the message: several codes share a status, and messages can be reworded.
Success
A successful response wraps its result in data:
{ "data": { "id": 101, "name": "Oak desk lamp", "slug": "oak-desk-lamp" } }Lists add links and meta for pagination. Some responses add a meta object with extra facts, such as meta.order_access_token after a guest checkout or meta.shipping_option_cleared after an address change.
- Times are ISO 8601, in UTC.
- Money is always a money object, never a bare number.
- New fields can appear at any time, so ignore fields you do not know. See versioning.
Errors
Every error uses one envelope:
{
"error": {
"code": "COUPON_NOT_APPLICABLE",
"message": "Spend £20.00 more to use this code.",
"details": {
"code": ["Spend £20.00 more to use this code."],
"reason": "threshold",
"remaining": { "amount": 2000, "currency": "GBP", "formatted": "£20.00" }
}
}
}| Field | Meaning |
|---|---|
code | A stable, upper-case code. Always present. |
message | A sentence that names the problem and, where it can, the fix. Safe to log. Present on almost every error; a few store-identification errors carry only the code. |
details | Extra facts for that code, when there are any. |
Every response also carries an X-Request-Id header. Log it, and quote it if you contact support about a request.
Validation errors
422 VALIDATION_FAILED means the request was understood but some input was refused. details maps each refused field to a list of messages. Nested fields use dots:
{
"error": {
"code": "VALIDATION_FAILED",
"message": "The billing.postcode field is required. (and 1 more error)",
"details": {
"billing.postcode": ["The billing.postcode field is required."],
"billing.country_id": ["The billing.country_id field is required."]
}
}
}Show each message next to its field. A few business checks use VALIDATION_FAILED too, under a key that is not a request field: for example, an item that has run out of stock at checkout is reported under cart.
Error codes
Store and access
| Status | code | Meaning | What to do |
|---|---|---|---|
| 400 | TENANT_KEY_MISSING | No X-Tenant-Key header | Send the store key on every call |
| 404 | TENANT_NOT_FOUND | Unknown key, a suspended store, or a rotated key after its overlap | Use the store's current key |
| 402 | TENANT_PAYMENT_REQUIRED | The store is temporarily unavailable | Show a "shop temporarily unavailable" page. Do not retry the request. |
| 403 | ORIGIN_NOT_ALLOWED | A browser call from a website the store has not registered | Register the website, or call from your server. See stores and keys. |
| 403 | BROWSER_CALLS_DISABLED | A browser call while browser calls are switched off | Call from your server with the secret |
| 403 | TENANT_SECRET_REQUIRED | A server call without X-Tenant-Secret | Send the secret on every server call |
| 403 | TENANT_SECRET_INVALID | A wrong secret, or a secret-only route called without it | Use the current secret from Settings → Storefront API |
| 401 | UNAUTHENTICATED | A missing, expired or revoked bearer token | Sign the customer in again, or check the merchant API key |
| 403 | FORBIDDEN | The token is valid but not allowed to do this, for example a merchant API key without the ability | Use a key with the right ability |
| 404 | FEATURE_NOT_ENABLED | The store's plan does not include this feature, such as the shop | Hide the feature for this store |
| 404 | MODULE_NOT_ENABLED | A module, such as live chat, is not switched on for the store | Hide the feature for this store |
Requests
| Status | code | Meaning | What to do |
|---|---|---|---|
| 422 | VALIDATION_FAILED | Some input was refused | Show details next to the fields |
| 404 | NOT_FOUND | The resource does not exist, or is not visible to this caller | Check the id, slug or reference |
| 400 and others | HTTP_ERROR | A request the API could not accept, such as an unknown filter, sort or include, or the wrong HTTP method; it keeps its real status | Fix the request |
| 409 | IDEMPOTENCY_IN_PROGRESS | A request with this Idempotency-Key is still running | Retry shortly with the same key |
| 422 | IDEMPOTENCY_KEY_REUSED | This key was already used for a different request | Make a new key for a new action. See idempotency. |
| 429 | RATE_LIMITED | Too many requests | Wait for Retry-After. See rate limits. |
| 429 | STAGING_LIMIT | A staging store (where your plan includes them) has used its daily allowance | Wait for Retry-After, the end of the UTC day |
| 500 | SERVER_ERROR | Something went wrong on our side | Retry with backoff. Quote X-Request-Id if it persists. |
| 503 | CAPTCHA_UNAVAILABLE | A form's spam check could not get an answer | Retry the submission with the same Idempotency-Key |
Carts
| Status | code | Meaning | What to do |
|---|---|---|---|
| 404 | CART_NOT_FOUND | The cart token is unknown, expired, or its cart became an order | Create a new cart |
| 404 | CART_LINE_NOT_FOUND | The line id is not in this cart | Refresh the cart |
| 422 | COUPON_INVALID | No such code | Show message |
| 422 | COUPON_EXPIRED | The code has not started or has ended (details.reason) | Show message |
| 422 | COUPON_USED_UP | The code has reached its usage limit | Show message |
| 422 | COUPON_LIMIT_REACHED | This shopper has used the code as often as allowed | Show message |
| 422 | COUPON_NOT_APPLICABLE | Nothing in the basket qualifies (details.reason, and details.remaining for a spend threshold) | Show message |
| 422 | GIFT_LINE_LOCKED | A free gift's quantity cannot be changed | Let the shopper remove it instead |
| 422 | ADDRESS_NOT_FOUND | A saved address_id that is not the customer's, or sent without signing in (details.reason is not_found or sign_in_required) | Refresh the address book, or ask the shopper to sign in or type the address |
| 422 | SHIPPING_ADDRESS_REQUIRED | Shipping options were asked for before a shipping address was set | Send the shopper to the address step |
Checkout and payments
| Status | code | Meaning | What to do |
|---|---|---|---|
| 402 | PAYMENT_FAILED | The payment was declined or not taken; details.decline_code says why on a provider decline | See payments |
| 409 | SHIPPING_OPTION_CHANGED | The chosen shipping option is gone or its price changed | Show details.options and let the shopper choose again |
| 409 | PROMOTIONS_CHANGED | The basket's discounts changed since payment was set up | Show the new total from details.cart and set up payment again |
| 409 | PAYMENT_REFUNDED | The payment was taken but the order could not be placed, so it was refunded | Start a new checkout |
| 409 | PAYMENT_REFUND_PENDING | As above, with the refund still being retried | Do not ask the shopper to pay again for this cart |
| 409 | PAYMENT_ALREADY_USED | The payment already paid for another checkout | Set up a new payment |
| 409 | PAYMENT_SETUP_IN_PROGRESS | Another request is setting up this cart's PaymentIntent | Retry shortly |
| 422 | PAYMENT_GATEWAY_UNAVAILABLE | The gateway is unknown or not set up for this store | Only offer gateways from checkout/config |
| 403 | ORDER_ACCESS_TOKEN_INVALID | A guest order token is missing, tampered with or expired (details.reason is invalid or expired) | Ask for the order reference and email again |
Accounts, content and CRM
| Status | code | Meaning | What to do |
|---|---|---|---|
| 409 | ACCOUNT_SHARED | The account shares its customer record, so only the store can erase it | Ask the shopper to contact the store |
| 403 | PREVIEW_TOKEN_INVALID | A page preview link is invalid or expired (details.reason) | Ask the editor for a new preview link |
| 422 | CONSENT_CONFIRMATION_INVALID | A marketing consent confirmation link or code could not be confirmed | Ask the person to request a new confirmation |
| 404 | CONVERSATION_NOT_FOUND | The chat token does not match a conversation | Start a new conversation |
| 409 | CONTACT_EXISTS | A CRM contact with this email already exists; details.id names it | Update that contact instead |
| 409 | ORDER_TRANSITION_NOT_ALLOWED | An order status change the order's current state does not allow | Check the order's state first |
| 409 | REFUND_NOT_ALLOWED | A refund the order's payments do not allow | Check the amount and the order's payments |
Which errors to retry
| Retry | Errors |
|---|---|
Yes, after a short wait, with the same Idempotency-Key | 500 SERVER_ERROR, network errors and timeouts, 409 IDEMPOTENCY_IN_PROGRESS, 409 PAYMENT_SETUP_IN_PROGRESS, 503 CAPTCHA_UNAVAILABLE |
Yes, after Retry-After | 429 RATE_LIMITED, 429 STAGING_LIMIT |
| Only after the shopper changes something | 402 PAYMENT_FAILED, the 409 checkout conflicts, 422 errors |
| No | 400, 401, 403 and 404 errors: the same request gets the same answer |
Use exponential backoff with jitter for automatic retries, and stop after a few attempts.
const res = await fetch(url, init)
if (!res.ok) {
const { error } = await res.json()
switch (error.code) {
case 'VALIDATION_FAILED':
return showFieldErrors(error.details)
case 'CART_NOT_FOUND':
return startNewCart()
case 'RATE_LIMITED':
return retryAfter(Number(res.headers.get('Retry-After') ?? 1))
default:
console.error(error.code, error.message, res.headers.get('X-Request-Id'))
return showGenericError()
}
}Search
Results pages, instant search suggestions, the merchant's search settings, the anonymous search events beacon, synonyms and search rate limits.
Pagination
Page through lists with page and perPage, read links and meta, use cursors on merchant lists, and sync CRM records incrementally with filter[updated_since].