> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.voodoo.center/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.voodoo.center/_mcp/server.

# Errors

## Error envelope

API errors return a standard JSON envelope with a human-readable `detail` and a
stable, machine-readable `code`:

```json title="Error response"
{
  "detail": "Insufficient balance",
  "code": "insufficient_balance"
}
```

Branch your error handling on `code` (stable) rather than `detail` (human-facing
text that may change).

The **token-exchange** endpoint (`POST /api/v1/auth/token/client`) uses a
simpler shape for auth failures: `{"error": "Invalid credentials"}` on `401`.
Every other endpoint uses the `detail` / `code` envelope above.

## Status codes

| HTTP  | `code`                 | When                                                                                                                                             |
| ----- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `400` | `validation_error`     | Bad or missing field, unknown/unavailable item (including out of stock at create time), duplicate `merchant_order_id`, or out-of-range quantity. |
| `400` | `insufficient_balance` | Your account balance can't cover the order.                                                                                                      |
| `401` | `not_authenticated`    | Missing, invalid or expired access token. Re-[exchange your API key](/authentication).                                                           |
| `404` | `not_found`            | Order doesn't exist, or isn't owned by your account.                                                                                             |
| `409` | `already_exists`       | Conflicts with an existing resource.                                                                                                             |
| `5xx` | —                      | Backend error. Retry with backoff; use a `merchant_order_id` so retries stay idempotent.                                                         |

### Examples

```json title="400 — validation error"
{ "detail": "Item is not available", "code": "validation_error" }
```

```json title="400 — insufficient balance"
{ "detail": "Insufficient balance", "code": "insufficient_balance" }
```

```json title="401 — not authenticated"
{ "detail": "Authentication credentials were not provided.", "code": "not_authenticated" }
```

```json title="404 — not found"
{ "detail": "Not found.", "code": "not_found" }
```

## Create-time errors vs. async failures

There are two distinct kinds of "failure", and they surface in different places:

* **Create-time (HTTP) errors** — the request is **rejected** and nothing is
  charged. These come back as a `4xx`/`5xx` with the error envelope, at the
  moment you call `POST /api/v1/orders`. Example: an item that is **out of
  stock at create time** returns `400 validation_error` ("Item is not
  available").

* **Async failures (terminal status)** — the order was **accepted** (`201`,
  `status: "pending"`) and your balance was charged, but fulfillment later fails
  or only partially succeeds. This is **not** an HTTP error. Instead the order
  settles on a terminal `status` of `failed` (with `error` / `error_message`,
  e.g. `OUT_OF_STOCK`, and a full `refund_amount`) or `partial` (with a
  `refund_amount` for the undelivered units). You learn this from your
  [webhook](/webhooks) or `GET /api/v1/orders/{id}`.

Rule of thumb: if you got a `201`, the order exists and was charged — watch its
`status`, not the HTTP response, for the outcome. If you got a `4xx`, nothing
happened and you can safely fix the request and retry.

## Retrying safely

Always send a unique `merchant_order_id` on `POST /api/v1/orders`. It is unique
per account, so if a retry (after a timeout or `5xx`) reuses the same value,
you won't accidentally place a duplicate order — a duplicate is rejected with a
`400 validation_error`.

Fix `401`s by re-exchanging your API key for a fresh token.

Where async `failed` / `partial` outcomes are delivered.