> ## Documentation Index
> Fetch the complete documentation index at: https://docs.craveup.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Error Codes

> All Crave API error codes, their meanings, and how to resolve them.

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:

```json theme={null}
{
  "error": "VALIDATION_ERROR",
  "message": "The 'quantity' field must be a positive integer.",
  "statusCode": 400
}
```

| Field        | Type     | Description                 |
| ------------ | -------- | --------------------------- |
| `error`      | `string` | Machine-readable error code |
| `message`    | `string` | Human-readable description  |
| `statusCode` | `number` | HTTP status code            |

## Handling errors with the SDK

The SDK throws an `ApiError` for any non-2xx response:

```ts theme={null}
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.

| Error                        | Message                                 | Resolution                                                                                             |
| ---------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `VALIDATION_ERROR`           | Field-specific validation message       | Check the `message` for which field failed. Ensure required fields are present and types match.        |
| `INVALID_PRODUCT`            | Product ID not found or unavailable     | Verify the `productId` exists and is available in the current menu.                                    |
| `INVALID_MODIFIER_SELECTION` | Modifier selection violates group rules | Check the modifier group's `min`/`max` rules. Ensure the number of selected options is within range.   |
| `CART_LOCKED`                | Cart is locked for payment processing   | The cart cannot be modified while payment is in progress. Wait for completion or create a new session. |
| `INVALID_DISCOUNT_CODE`      | Discount code is invalid or expired     | Show the user that the promo code could not be applied. Verify the code and try again.                 |
| `MINIMUM_ORDER_NOT_MET`      | Order does not meet the minimum amount  | Display the minimum order requirement and prompt the user to add more items.                           |

### 401 — Unauthorized

Authentication failed.

| Error                | Message                                   | Resolution                                       |
| -------------------- | ----------------------------------------- | ------------------------------------------------ |
| `MISSING_API_KEY`    | No API key provided                       | Include the `X-API-Key` header in your request.  |
| `INVALID_API_KEY`    | API key is invalid or revoked             | Generate a new API key from the Dashboard.       |
| `INVALID_AUTH_TOKEN` | Customer auth token is invalid or expired | Refresh the customer's JWT or redirect to login. |

### 403 — Forbidden

The API key doesn't have permission for this operation.

| Error                   | Message                                     | Resolution                                                                     |
| ----------------------- | ------------------------------------------- | ------------------------------------------------------------------------------ |
| `LOCATION_MISMATCH`     | API key is not authorized for this location | Use the API key scoped to the target location. Each key is location-specific.  |
| `OPERATION_NOT_ALLOWED` | This operation is not permitted             | Check that the endpoint is available for storefront API keys (vs. admin keys). |

### 404 — Not Found

The requested resource doesn't exist.

| Error                | Message                                  | Resolution                                                                        |
| -------------------- | ---------------------------------------- | --------------------------------------------------------------------------------- |
| `LOCATION_NOT_FOUND` | Location ID does not exist               | Verify the `locationId` parameter. Check the Dashboard for valid IDs.             |
| `CART_NOT_FOUND`     | Cart ID does not exist or has expired    | Create a new ordering session to get a fresh cart. Carts expire after inactivity. |
| `PRODUCT_NOT_FOUND`  | Product not available in current menu    | The product may be time-restricted. Check the active menu for the current time.   |
| `MERCHANT_NOT_FOUND` | Merchant slug does not match any account | Verify the slug used in `merchant.getBySlug()`.                                   |

### 409 — Conflict

The request conflicts with the current state.

| Error                      | Message                                          | Resolution                                                |
| -------------------------- | ------------------------------------------------ | --------------------------------------------------------- |
| `DISCOUNT_ALREADY_APPLIED` | A discount is already applied to this cart       | Remove the existing discount before applying a new one.   |
| `SESSION_ALREADY_EXISTS`   | An ordering session already exists for this cart | Reuse the existing session instead of creating a new one. |

### 422 — Unprocessable Entity

The request was well-formed but the data cannot be processed.

| Error                       | Message                                        | Resolution                                                                                                            |
| --------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `LOCATION_CLOSED`           | Location is not accepting orders at this time  | Check the order times endpoint and display operating hours to the user.                                               |
| `DELIVERY_OUT_OF_RANGE`     | Delivery address is outside the delivery zone  | Show the user that their address is too far. Use `locations.getDistance()` to check distance before setting delivery. |
| `FULFILLMENT_NOT_AVAILABLE` | The selected fulfillment method is not enabled | Check `location.methodsStatus` and only show available methods.                                                       |
| `INVALID_ORDER_TIME`        | The requested order time is not available      | Fetch fresh order times and let the user pick a valid slot.                                                           |

### 429 — Too Many Requests

You've exceeded the rate limit.

| Error                 | Message           | Resolution                                                                                   |
| --------------------- | ----------------- | -------------------------------------------------------------------------------------------- |
| `RATE_LIMIT_EXCEEDED` | Too many requests | Back off and retry. Check the `Retry-After` or `X-RateLimit-Reset` header for when to retry. |

**Rate limit headers:**

| Header                  | Description                           |
| ----------------------- | ------------------------------------- |
| `X-RateLimit-Limit`     | Maximum requests per window           |
| `X-RateLimit-Remaining` | Requests remaining in current window  |
| `X-RateLimit-Reset`     | Unix timestamp when the window resets |

### 500 — Internal Server Error

An unexpected error on the Crave server.

| Error            | Message                      | Resolution                                                              |
| ---------------- | ---------------------------- | ----------------------------------------------------------------------- |
| `INTERNAL_ERROR` | An unexpected error occurred | Retry with exponential backoff. If the error persists, contact support. |

### 502 / 503 — Service Unavailable

The API or an upstream service is temporarily unavailable.

| Error                 | Message                                | Resolution                                                                                 |
| --------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------ |
| `SERVICE_UNAVAILABLE` | The service is temporarily unavailable | Retry after a few seconds. Check the [Crave status page](https://craveup.com) for outages. |

## Retry strategy

For transient errors (429, 500, 502, 503), implement exponential backoff:

```ts theme={null}
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:

```ts theme={null}
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

<CardGroup cols={2}>
  <Card title="SDK Reference" icon="code" href="/getting-started/storefront-sdk">
    Full SDK documentation with error handling examples.
  </Card>

  <Card title="REST API" icon="square-terminal" href="/getting-started/rest-api">
    REST endpoint reference and tips.
  </Card>
</CardGroup>

<Snippet file="need-help.mdx" />
