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

# Errors

> The error envelope, every HTTP status and stable error code with what to do about it, batch item errors, ambiguous failures, and the X-Request-Id header.

## The envelope

Every non-2xx response, and every error item inside a batch `200`, has the same two fields:

```json theme={null}
{ "error": "invalid_request", "message": "element and user_id are required" }
```

`error` is a stable code for your code to branch on; `message` is the human-readable reason, the same text this documentation quotes. Internal faults add `request_id`:

```json theme={null}
{ "error": "internal_error", "message": "Internal error", "request_id": "2f7e9c1a-5b3d-4e8f-9a0b-1c2d3e4f5a6b" }
```

## Statuses

| Status | Retry? | Meaning |
| - | - | - |
| `200` | | Success. In a batch, individual items may still carry an error. |
| `400` | No. Fix the request. | The request is wrong. Nothing was written. |
| `401` | No. Fix the key. | The key is missing, malformed, unknown or revoked. |
| `403` | No. Use the right key or origin. | The key may not do this, or came from a disallowed origin. |
| `404`, `405` | No. | Wrong path or method; the data plane is four `POST` endpoints. |
| `409` | Only `lock_timeout`, once. | The request conflicts with current state. |
| `429` | After `Retry-After`. | Over the per-minute limit; `Retry-After` says how long. |
| `500` | `decide`: serve your fallback. `events`, `identify`: retry with the same ids. | A fault on our side, sanitized. |
| `502` and other `5xx` | Only with the same `idempotency_key` or `id`. | The request may or may not have been applied. |

## Codes

The `error` field carries one of these codes. Each section gives the status, what happened, and what to do.

### `invalid_request`

`400`. A validation failure. The message names the field, with its path for nested ones (`Received unknown parameter: rewards[0].window_second`).

Fix the field the message names. Field names are `snake_case` and match the [contract](https://docs.trycurious.ai/api-reference/overview.md). See [A request is rejected for an unknown field](https://docs.trycurious.ai/troubleshooting/unknown-field.md).

### `invalid_json`

`400`. The body could not be parsed (`Invalid JSON body`).

Check the serializer and the `Content-Type` header.

### `unauthorized`

`401`. `Missing or invalid API key`, or `API key has been revoked`.

Send `Authorization: Bearer <key>` with a current key. After a rotation, deploy the new key before revoking the old.

### `forbidden`

`403`. `Origin not allowed for this key`: a publishable key from an origin not on its allowlist. Or a publishable key on a secret-only endpoint: `Declare requires a secret key`.

Add the exact `https://` origin to the key in the dashboard, or use a secret key from a server.

### `not_found`

`404`. No such path.

Use `/v1/decide`, `/v1/declare`, `/v1/events` or `/v1/identify`.

### `method_not_allowed`

`405`. Only `POST` is accepted.

Use `POST`.

### `conflict`

`409`. State conflicts with the request. Three cases: `No live experiment for element "…"` under a publishable key; `Identity conflicts with an existing user` on `identify`; `Experiment already has 50 active arms`.

Declare the element with a secret key first; resolve the identity link on your side; conclude an arm before adding another. Do not retry. See [The API returns 409 conflict](https://docs.trycurious.ai/troubleshooting/conflict-409.md).

### `lock_timeout`

`409`. `Identity changed during selection; retry the request`: the user's identity was being linked while this decision was selecting an arm. The one retryable `409`.

Retry the same request once. See [The API returns 409 conflict](https://docs.trycurious.ai/troubleshooting/conflict-409.md).

### `idempotency_error`

`409`. `idempotency_key was already used with a different request`.

Generate a new key for a new logical request; reuse a key only for a byte-for-byte retry. See [Idempotency](https://docs.trycurious.ai/api-reference/idempotency.md).

### `rate_limit_exceeded`

`429`. Over 1,000 requests per minute per endpoint for the organization.

Wait `Retry-After` seconds, then retry with the same keys. Batch events. See [Rate limits](https://docs.trycurious.ai/api-reference/rate-limits.md).

### `internal_error`

`500`, `502` or another `5xx`. A sanitized fault on our side, or the API edge could not complete the call. Carries `request_id`.

For `decide`, serve your fallback and retry only with the same `idempotency_key`. For `events`, retry with the same `id`. For `identify`, retry the same request. Quote `request_id` to [support](mailto:team@trycurious.ai).

## Batch items

In a batch, a failing item is reported inside the `200` at its index with the same shape:

```json theme={null}
{ "index": 2, "element": "hero_cta", "status": "error", "error": "invalid_request", "message": "control is required when creating an experiment" }
```

Envelope-level errors apply to the whole request (a bad key, a malformed envelope, more items than the cap) and arrive as a non-2xx status. Item-level errors apply to one item and never fail its siblings. See [Batch calls](https://docs.trycurious.ai/guides/batch.md).

## Ambiguous failures

A `502`, any other `5xx` from the API edge, or a dropped connection means the request may or may not have been applied. The message says so and names the safe retry:

```text theme={null}
The API could not be reached, and the request may or may not have been applied; retry only with the same idempotency_key (decide) or id (events)
```

An unkeyed retry after such a failure can duplicate the write. The server SDK therefore never retries `decide`, and retries events only under their stable ids.

## `X-Request-Id`

Every response, success or failure, carries an `X-Request-Id` header: the id of that request on our side. Quote it when you [contact support](mailto:team@trycurious.ai) and we can find the exact request in our logs. Internal faults repeat it in the body as `request_id`. Browsers can read the header; it is exposed through CORS. The SDK surfaces it as `requestId` on every result and error.

## In the server SDK

The SDK maps these codes and statuses onto its own stable codes with a `retryable` flag, and never exposes a raw body. The mapping is on [Errors in the server SDK](https://docs.trycurious.ai/sdks/node/errors.md).


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