Skip to main content
The Crave Storefront API returns standard HTTP status codes with structured error bodies. This reference lists every error you may encounter and how to handle each one.

Error response format

All error responses follow this structure:
{
  "error": "VALIDATION_ERROR",
  "message": "The 'quantity' field must be a positive integer.",
  "statusCode": 400
}
FieldTypeDescription
errorstringMachine-readable error code
messagestringHuman-readable description
statusCodenumberHTTP status code

Handling errors with the SDK

The SDK throws an ApiError for any non-2xx response:
import { ApiError } from '@craveup/storefront-sdk';

try {
  await storefront.cart.addItem(locationId, cartId, payload);
} catch (error) {
  if (error instanceof ApiError) {
    console.error({
      status: error.status,       // e.g. 400
      statusText: error.statusText, // e.g. "Bad Request"
      url: error.url,             // the request URL
      body: error.body,           // raw response body (string)
    });

    // Parse the body for structured error info
    const parsed = JSON.parse(error.body ?? '{}');
    showToast(parsed.message);
  }
}

HTTP status codes

400 — Bad Request

The request body or query parameters are invalid.
ErrorMessageResolution
VALIDATION_ERRORField-specific validation messageCheck the message for which field failed. Ensure required fields are present and types match.
INVALID_PRODUCTProduct ID not found or unavailableVerify the productId exists and is available in the current menu.
INVALID_MODIFIER_SELECTIONModifier selection violates group rulesCheck the modifier group’s min/max rules. Ensure the number of selected options is within range.
CART_LOCKEDCart is locked for payment processingThe cart cannot be modified while payment is in progress. Wait for completion or create a new session.
INVALID_DISCOUNT_CODEDiscount code is invalid or expiredShow the user that the promo code could not be applied. Verify the code and try again.
MINIMUM_ORDER_NOT_METOrder does not meet the minimum amountDisplay the minimum order requirement and prompt the user to add more items.

401 — Unauthorized

Authentication failed.
ErrorMessageResolution
MISSING_API_KEYNo API key providedInclude the X-API-Key header in your request.
INVALID_API_KEYAPI key is invalid or revokedGenerate a new API key from the Dashboard.
INVALID_AUTH_TOKENCustomer auth token is invalid or expiredRefresh the customer’s JWT or redirect to login.

403 — Forbidden

The API key doesn’t have permission for this operation.
ErrorMessageResolution
LOCATION_MISMATCHAPI key is not authorized for this locationUse the API key scoped to the target location. Each key is location-specific.
OPERATION_NOT_ALLOWEDThis operation is not permittedCheck that the endpoint is available for storefront API keys (vs. admin keys).

404 — Not Found

The requested resource doesn’t exist.
ErrorMessageResolution
LOCATION_NOT_FOUNDLocation ID does not existVerify the locationId parameter. Check the Dashboard for valid IDs.
CART_NOT_FOUNDCart ID does not exist or has expiredCreate a new ordering session to get a fresh cart. Carts expire after inactivity.
PRODUCT_NOT_FOUNDProduct not available in current menuThe product may be time-restricted. Check the active menu for the current time.
MERCHANT_NOT_FOUNDMerchant slug does not match any accountVerify the slug used in merchant.getBySlug().

409 — Conflict

The request conflicts with the current state.
ErrorMessageResolution
DISCOUNT_ALREADY_APPLIEDA discount is already applied to this cartRemove the existing discount before applying a new one.
SESSION_ALREADY_EXISTSAn ordering session already exists for this cartReuse the existing session instead of creating a new one.

422 — Unprocessable Entity

The request was well-formed but the data cannot be processed.
ErrorMessageResolution
LOCATION_CLOSEDLocation is not accepting orders at this timeCheck the order times endpoint and display operating hours to the user.
DELIVERY_OUT_OF_RANGEDelivery address is outside the delivery zoneShow the user that their address is too far. Use locations.getDistance() to check distance before setting delivery.
FULFILLMENT_NOT_AVAILABLEThe selected fulfillment method is not enabledCheck location.methodsStatus and only show available methods.
INVALID_ORDER_TIMEThe requested order time is not availableFetch fresh order times and let the user pick a valid slot.

429 — Too Many Requests

You’ve exceeded the rate limit.
ErrorMessageResolution
RATE_LIMIT_EXCEEDEDToo many requestsBack off and retry. Check the Retry-After or X-RateLimit-Reset header for when to retry.
Rate limit headers:
HeaderDescription
X-RateLimit-LimitMaximum requests per window
X-RateLimit-RemainingRequests remaining in current window
X-RateLimit-ResetUnix timestamp when the window resets

500 — Internal Server Error

An unexpected error on the Crave server.
ErrorMessageResolution
INTERNAL_ERRORAn unexpected error occurredRetry with exponential backoff. If the error persists, contact support.

502 / 503 — Service Unavailable

The API or an upstream service is temporarily unavailable.
ErrorMessageResolution
SERVICE_UNAVAILABLEThe service is temporarily unavailableRetry after a few seconds. Check the Crave status page for outages.

Retry strategy

For transient errors (429, 500, 502, 503), implement exponential backoff:
async function withRetry<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (
        error instanceof ApiError &&
        [429, 500, 502, 503].includes(error.status) &&
        attempt < maxRetries
      ) {
        const delay = Math.min(1000 * 2 ** attempt, 10_000);
        await new Promise((resolve) => setTimeout(resolve, delay));
        continue;
      }
      throw error;
    }
  }
  throw new Error('Max retries exceeded');
}

// Usage
const cart = await withRetry(() => storefront.cart.get(locationId, cartId));

User-facing error messages

Map API errors to friendly messages for your UI:
function getUserMessage(error: ApiError): string {
  const body = JSON.parse(error.body ?? '{}');

  switch (error.status) {
    case 400:
      return body.message ?? 'Please check your input and try again.';
    case 401:
      return 'Your session has expired. Please refresh the page.';
    case 404:
      return 'This item is no longer available.';
    case 409:
      return body.message ?? 'This action conflicts with your current order.';
    case 422:
      return body.message ?? 'We cannot process this request right now.';
    case 429:
      return 'Too many requests. Please wait a moment and try again.';
    default:
      return 'Something went wrong. Please try again.';
  }
}

Next steps