Handling Errors
Error shape
Every API error returns a consistent JSON body:
{
"status": "failed",
"message": "Human-readable description",
"code": "MACHINE_READABLE_CODE"
}
Always branch on code, never on message. Codes are stable across releases; messages may be updated.
Shop-level errors
These indicate the shop is not currently serving customers. Check for them on every initial data fetch and redirect before rendering any shop UI.
SHOP_UNDER_DEVELOPMENT (403)
The merchant toggled the shop into maintenance mode.
if (error.code === 'SHOP_UNDER_DEVELOPMENT') {
return redirect('/coming-soon');
}
What your theme must show: A branded coming-soon or maintenance page. Do not render any product data, prices, or shop UI, or attempt to call any other endpoint.
SHOP_BILLING_INVALID (403)
The merchant's subscription has lapsed.
if (error.code === 'SHOP_BILLING_INVALID') {
return redirect('/coming-soon');
}
What your theme must show: A branded coming-soon or maintenance page. Do not render any product data, prices, or shop UI, or attempt to call any other endpoint.
ORIGIN_NOT_ALLOWED (401)
Your theme's domain is not in the API key's allowed origins list.
This should never reach customers. Fix it in the admin: Settings → Developer Tools → API Keys → Edit key → Add domain.
Resource errors
These happen during normal browsing. Handle at the page level.
| Code | HTTP | When it happens | What to show |
|---|---|---|---|
PRODUCT_NOT_FOUND | 404 | Product was unpublished after the URL was shared | Your 404 page |
CATEGORY_NOT_FOUND | 404 | Category slug no longer exists | Your 404 page |
ORDER_NOT_FOUND | 404 | Order ID invalid or belongs to a different user | 404 or redirect to orders list |
VARIANT_NOT_FOUND | 404 | Variation was deleted after it was added to cart | Inline cart error, prompt to remove item |
CONTENT_MODEL_NOT_FOUND | 404 | Model slug doesn't exist in the admin | Fail silently or show nothing |
CONTENT_ENTRY_NOT_FOUND | 404 | Entry was unpublished | Fail silently or redirect |
POLICY_NOT_FOUND | 404 | Policy type not configured | "This policy is not available" |
Customer auth errors
| Code | What to do |
|---|---|
SESSION_EXPIRED | Clear token, redirect to /login |
AUTH_TOKEN_INVALID | Clear token, redirect to /login |
AUTH_TOKEN_MISSING | Redirect to /login |
ACCOUNT_BLACKLISTED | Show "your account has been suspended, contact support" |
ACCOUNT_INACTIVE | Show "your account is not active, contact shop support" |
TOO_MANY_LOGIN_ATTEMPTS | Show "too many attempts, please wait before trying again" |
TOO_MANY_VERIFY_ATTEMPTS | Show "too many attempts, please request a new code" |
Checkout and order errors
| Code | What to show |
|---|---|
ORDER_VALIDATION_FAILED | "Could not place your order, please review your cart" |
INVALID_SHIPPING_ID | "Selected shipping method is no longer available" |
INVALID_COUPON | "This coupon code is not valid" |
COUPONS_DISABLED | Remove coupon input from checkout UI |
PAYMENTS_NOT_ENABLED | "Online payments are not available for this store" |
Rate limiting
The API applies a 500 requests per 15-minute window per API key. Contact form and newsletter have tighter limits (3 and 5 per hour respectively).
When rate-limited the API returns HTTP 429. Show a "please try again shortly" message.
Global error handler pattern
async function apiRequest(url, options = {}) {
const res = await fetch(`${BASE_URL}${url}`, {
...options,
headers: { ...API_HEADERS, ...options.headers },
});
if (!res.ok) {
const error = await res.json().catch(() => ({ code: 'UNKNOWN_ERROR' }));
// Shop-level: redirect before any UI renders
if (error.code === 'SHOP_UNDER_DEVELOPMENT') redirect('/coming-soon');
if (error.code === 'SHOP_BILLING_INVALID') redirect('/coming-soon');
// Auth: clear token and redirect
if (['SESSION_EXPIRED', 'AUTH_TOKEN_INVALID', 'AUTH_TOKEN_MISSING'].includes(error.code)) {
clearToken();
redirect('/login');
}
throw error; // let the page handle the rest
}
return res.json();
}