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

# Errors

> The v2 error envelope, every code we send, and how to handle them.

Every error response uses the same envelope:

```json theme={null}
{
  "error": {
    "code": "validation_failed",
    "message": "Validation failed: title is required.",
    "details": [{ "path": "title", "message": "Required" }],
    "request_id": "req_3727ca7b4f0fff3c"
  }
}
```

* **`code`** - stable, machine-readable. Switch on this.
* **`message`** - human-readable summary. Safe to surface to operators; not always safe to surface to end users verbatim.
* **`details`** - optional, code-specific structure (validation paths, required scopes, rate-limit info).
* **`request_id`** - also returned as the `X-Request-Id` header. Quote it in support tickets and we can pull the exact request from our logs.

Every response - success or error - carries `X-Request-Id`. Logging this header on the client side makes triage trivial.

## Codes

| HTTP | Code                      | When                                                                                         |
| ---- | ------------------------- | -------------------------------------------------------------------------------------------- |
| 400  | `validation_failed`       | Body or query failed validation. `details` has the per-field reasons.                        |
| 401  | `unauthenticated`         | Missing or invalid bearer token.                                                             |
| 403  | `scope_required`          | Key doesn't have the scope this endpoint requires. `details` lists `required` and `granted`. |
| 403  | `forbidden`               | Authenticated but blocked (plan gating, deleted resource, etc.).                             |
| 404  | `not_found`               | Resource doesn't exist or has been deleted.                                                  |
| 409  | `conflict`                | Semantic conflict (e.g. closing an already-closed live chat).                                |
| 410  | `unsupported_key_version` | You passed a v1 key to a v2 endpoint.                                                        |
| 422  | `unprocessable`           | Well-formed request we still can't process (e.g. malformed cursor).                          |
| 429  | `rate_limited`            | Too many requests. See [Rate limits](/api-v2/rate-limits).                                   |
| 500  | `internal_error`          | Our problem. Retry with backoff; if it persists, share the `request_id`.                     |

## Details shapes

The `details` field varies by code. The shapes you'll actually inspect:

### `validation_failed`

Array of per-field reasons:

```json theme={null}
"details": [
  { "path": "title",   "message": "Required" },
  { "path": "tags.0",  "message": "Expected string, received number" }
]
```

`path` is a dotted path through the request body. Use it to highlight the offending field in your UI.

### `scope_required`

```json theme={null}
"details": { "required": ["threads:write"], "granted": ["threads:read"] }
```

Tells you exactly which scope to add to the key.

### `rate_limited`

```json theme={null}
"details": { "limit": 60, "window": "1m" }
```

The response also includes a `Retry-After` header - prefer that for retry timing.

## Handling errors

```typescript theme={null}
type ApiError = {
  error: {
    code: string;
    message: string;
    details?: unknown;
    request_id: string;
  };
};

const res = await fetch(url, {
  headers: { Authorization: `Bearer ${KEY}` },
});

if (!res.ok) {
  const body = (await res.json()) as ApiError;
  switch (body.error.code) {
    case "rate_limited": {
      const retryAfter = Number(res.headers.get("Retry-After") ?? "1");
      await new Promise((r) => setTimeout(r, retryAfter * 1000));
      // retry
      break;
    }
    case "scope_required":
      // tell the operator to widen the key's scopes
      break;
    case "validation_failed":
      // surface body.error.details to the user
      break;
    case "unsupported_key_version":
      // you passed a v1 key - mint a v2 one
      break;
    default:
      console.error(
        "Productlane error",
        body.error.code,
        body.error.request_id,
      );
  }
}
```

## Retry strategy

| Code             | Retry?                                                                               |
| ---------------- | ------------------------------------------------------------------------------------ |
| `rate_limited`   | Yes - honour `Retry-After`.                                                          |
| `internal_error` | Yes - exponential backoff (1s, 2s, 4s, …), give up after a few attempts.             |
| `conflict`       | Sometimes - only if your retry resolves the conflict (e.g. re-fetch then re-submit). |
| Everything else  | **No.** A retry will fail the same way. Fix the request.                             |
