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

# Errors and limits

> Error response shape, status codes and machine codes, rate limits and safe retries.

The whole API returns responses in one fixed shape. This page shows how to recognise an error, when to retry it and how to stay within the limits.

## Response shape

Success:

```json theme={null}
{ "success": true, "data": { "...": "..." }, "message": "optional" }
```

Some endpoints add context fields — e.g. creating an order returns `redirectUrl`, and a free order returns `free: true`. Paginated lists return `data`, `total`, `page`, `pages`, `hasNext`, `hasPrev`.

Error:

```json theme={null}
{
  "success": false,
  "statusCode": 409,
  "error": "This Idempotency-Key was already used for a different checkout",
  "code": "IDEMPOTENCY_KEY_REUSED",
  "timestamp": "2026-10-27T10:30:00.000Z",
  "path": "/api/v2/orders"
}
```

| Field | Description |
| - | - |
| `success` | always `false` on error — check it rather than only the HTTP status |
| `statusCode` | HTTP status |
| `error` | human-readable message (for logs, not to show verbatim to users) |
| `code` | optional, stable machine code — translate your UI message from it |
| `details` | optional, on validation errors: the list of invalid fields |
| `timestamp`, `path` | time and path (without sensitive query data) |

## Status codes

| Code | Meaning | Typical cause | Retry? |
| - | - | - | - |
| `400` | Bad Request | validation error, bad id format (an ObjectId is 24 hex chars) | no — fix the request |
| `401` | Unauthorized | missing, wrong, expired or revoked key; forged webhook | no |
| `403` | Forbidden | key scope too small, different shop, missing role, captcha required | no |
| `404` | Not Found | resource doesn't exist or doesn't belong to this shop | no |
| `409` | Conflict | state conflict: idempotency, checkout in progress, legal documents changed | depends on `code` |
| `429` | Too Many Requests | rate limit exceeded | yes, after a delay |
| `500` | Internal Server Error | server-side error | yes, with backoff |
| `503` | Service Unavailable | shop temporarily busy (`SHOP_BUSY`) | yes, with backoff |

## Machine codes

Selected `code` values worth handling in code:

| `code` | HTTP | What to do |
| - | - | - |
| `IDEMPOTENCY_KEY_REQUIRED` | `400` | add an `Idempotency-Key` header (32–128 chars of `A-Z a-z 0-9 _ -`) |
| `IDEMPOTENCY_KEY_REUSED` | `409` | the key was used for a different body — generate a new one |
| `CHECKOUT_ATTEMPT_CLOSED` | `409` | the purchase attempt is closed — start a new one with a new key |
| `CHECKOUT_IN_PROGRESS` | `409` | the same purchase is being processed — wait and retry with the same key |
| `LEGAL_CONSENT_REQUIRED` | `400` | required checkout consents are missing |
| `LEGAL_DOCUMENTS_CHANGED` | `409` | the terms changed meanwhile — fetch the current version and ask for consent again |
| `CAPTCHA_REQUIRED` | `403` | show the Turnstile widget and send its token |
| `PRODUCT_VARIANT_REQUIRED` / `INVALID_PRODUCT_VARIANT` | `400` | pick a valid product variant |
| `SHOP_PAYMENT_NOT_READY` | `400` | the shop has no ready payment method |
| `PAYMENT_INIT_FAILED` | `400` | the gateway refused to create the payment — try another method |
| `SHOP_NOT_ACCEPTING_MUTATIONS` | `409` | the shop is in an operation that blocks changes |
| `SHOP_BUSY` | `503` | retry shortly |
| `CART_TOO_LARGE` | `400` | a cart holds 1–20 lines — split the purchase |
| `ORDER_NOT_FOUND` | `404` | wrong order number or status key (Storefront v1 status/delivery) |
| `ORDER_NOT_PAID` | `409` | digital content is released only after payment |
| `DELIVERY_REVOKED` | `410` | the order was refunded or disputed — the content is no longer available |
| `NO_DIGITAL_DELIVERY` | `404` | the order has no digital content |
| `DOWNLOAD_EXPIRED` / `DOWNLOAD_LIMIT_REACHED` / `DOWNLOAD_UNAVAILABLE` | `410` | the file link expired, its downloads are used up or the file is gone — request a new link from the seller |
| `VOUCHER_NOT_FOUND` / `VOUCHER_USED` / `VOUCHER_EXPIRED` | `400` | unknown or disabled / already redeemed / expired voucher (an unknown and a disabled code answer the same on purpose) |
| `WORKSPACE_PRODUCTION_REQUIRES_APPROVAL` | `403` | production publishing from a key, CLI, MCP or AI agent needs approval — promote in the panel or turn the policy off in **Workspace → Publishing** |
| `SITE_BUILD_BUSY` | `409` | too many active builds for the plan (Free: 1, Pro: 2) — wait for one to finish |
| `SITE_BUILD_RATE_LIMIT` | `429` | hourly build limit (Free: 10/h, Pro: 30/h) |
| `AI_USAGE_UNAVAILABLE` | `503` | the AI daily budget cannot be checked right now — retry shortly |

New codes may appear — treat an unknown `code` according to its `statusCode`.

## Validation

The API rejects invalid data before processing it. Fields outside the schema are ignored and types are converted automatically (e.g. numbers from the query string). Lists accept `page` and `limit`; `limit` is clamped to a safe maximum (usually 100).

## Rate limits

The whole REST API has a global limit per real client IP. Production configuration: **200 requests per minute** (`RATE_LIMIT_MAX=200`, `RATE_LIMIT_WINDOW=60000`); the platform operator can change both values.

Sensitive routes have additional, stricter fixed windows, counted per IP and route:

| Operation | Limit / minute |
| - | - |
| Create a public checkout | 12 |
| Public order status check | 30 |
| Send a nickname-change code | 5 |
| Verify a nickname-change code | 10 |
| Redeem a voucher | 8 |
| Storefront v1: checkout / voucher redemption | 12 |
| Storefront v1: collect digital content | 20 |
| Check a voucher exists | 20 |
| Validate / check a discount | 30 |
| Create an API key | 10 |
| Payment session mutation | 15 |
| Public analytics beacon | 60 |

Login routes have their own brute-force limits. Payment webhooks are protected per provider and shop while keeping the provider's retry semantics.

> [!NOTE]
>
> Counters for sensitive routes are shared through Redis across API instances. If Redis is briefly unavailable, each instance keeps a bounded in-memory counter — protection doesn't disappear, it just becomes per-instance.

The game plugin doesn't poll REST — its Socket.IO connection has its own authentication, validation, debounce and event limits (see [Plugin protocol](https://andcze.mintlify.site/en/developers/plugin-protocol.md)).

## Retries

Retry only `429`, `500`, `503` and network errors. Don't retry `400`, `401`, `403`, `404`.

* If a `429` response has a `Retry-After` header, wait that many seconds. Not every response has it (stricter per-route limits don't send it) — then use backoff.
* Exponential backoff with jitter: e.g. 1 s, 2 s, 4 s ± random 0–500 ms, a few attempts at most.
* Never retry immediately in a loop — it only extends the block.

```ts theme={null}
async function withRetry<T>(fn: () => Promise<Response>, attempts = 4): Promise<T> {
  for (let i = 0; ; i++) {
    const res = await fn();
    if (res.ok) return (await res.json()) as T;
    const retryable = res.status === 429 || res.status >= 500;
    if (!retryable || i === attempts - 1) throw await res.json();
    const retryAfter = Number(res.headers.get("retry-after"));
    const delay = retryAfter > 0 ? retryAfter * 1000 : 2 ** i * 1000 + Math.random() * 500;
    await new Promise((r) => setTimeout(r, delay));
  }
}
```

## Idempotency

Creating an order (`POST /orders`, and from 3.0 also `POST /storefront/v1/shops/<shopId>/checkout`) **requires** an `Idempotency-Key` header:

* a random value of 32–128 chars (`A-Z a-z 0-9 _ -`), e.g. a UUID without dashes or base64url;
* generate it **once per purchase attempt** and reuse it only when retrying the identical request;
* a retry with the same key and the same body returns the same order instead of creating a second one;
* the same key with a different body → `409 IDEMPOTENCY_KEY_REUSED`; a closed attempt → `409 CHECKOUT_ATTEMPT_CLOSED`.

The server stores only the key's SHA-256 hash. On the payment side ItemShop is also idempotent towards repeated provider notifications — an order is marked paid exactly once.

## Next

> **[API with a key](https://andcze.mintlify.site/en/developers/api-keys.md)**
>
> Keys, scopes and your first request.

> **[Webhooks](https://andcze.mintlify.site/en/developers/webhooks.md)**
>
> Discord notifications and payment-provider webhooks.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.