Skip to main content

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.

CodeHTTPWhen it happensWhat to show
PRODUCT_NOT_FOUND404Product was unpublished after the URL was sharedYour 404 page
CATEGORY_NOT_FOUND404Category slug no longer existsYour 404 page
ORDER_NOT_FOUND404Order ID invalid or belongs to a different user404 or redirect to orders list
VARIANT_NOT_FOUND404Variation was deleted after it was added to cartInline cart error, prompt to remove item
CONTENT_MODEL_NOT_FOUND404Model slug doesn't exist in the adminFail silently or show nothing
CONTENT_ENTRY_NOT_FOUND404Entry was unpublishedFail silently or redirect
POLICY_NOT_FOUND404Policy type not configured"This policy is not available"

Customer auth errors

CodeWhat to do
SESSION_EXPIREDClear token, redirect to /login
AUTH_TOKEN_INVALIDClear token, redirect to /login
AUTH_TOKEN_MISSINGRedirect to /login
ACCOUNT_BLACKLISTEDShow "your account has been suspended, contact support"
ACCOUNT_INACTIVEShow "your account is not active, contact shop support"
TOO_MANY_LOGIN_ATTEMPTSShow "too many attempts, please wait before trying again"
TOO_MANY_VERIFY_ATTEMPTSShow "too many attempts, please request a new code"

Checkout and order errors

CodeWhat 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_DISABLEDRemove 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();
}