# Errors

> The Bookbag error envelope and the common machine-readable codes. How v2 and the Chatbase-compatible v1 surfaces report failures, plus rate limiting.

Bookbag uses conventional HTTP status codes and a consistent JSON envelope so you can branch on a stable, machine-readable code rather than parsing prose.

## The v2 error envelope

Every API v2 error response has this shape:

```json
{
  "error": {
    "code": "VALIDATION_INVALID_BODY",
    "message": "message is required and must be a non-empty string",
    "details": {}
  }
}
```

- `code` — a stable, screaming-snake-case identifier. Branch on this.
- `message` — a human-readable explanation. May change; do not match on it.
- `details` — present only on some errors (for example field-level validation issues).

The HTTP status reflects the class of error; the `code` gives the specific reason.

## Common codes

| Status | Code | When |
| --- | --- | --- |
| 400 | VALIDATION_INVALID_BODY | A required field is missing or a field failed validation (e.g. empty `message`, malformed `userId`). |
| 401 | AUTH_MISSING_API_KEY | No `Authorization: Bearer` header. |
| 401 | AUTH_INVALID_API_KEY | Unknown, malformed, or revoked key. |
| 403 | AUTH_INSUFFICIENT_PERMISSIONS | An agent-scoped key targeting a different agent. |
| 404 | RESOURCE_NOT_FOUND | The agent or conversation does not exist or is not in your workspace. |
| 404 | RESOURCE_MESSAGE_NOT_FOUND | The referenced message does not exist in the conversation. |
| 404 | RESOURCE_TOOL_CALL_NOT_FOUND | No pending tool call matched the supplied `toolCallId`. |
| 400 | RESOURCE_MESSAGE_NOT_ASSISTANT | Feedback was sent for a non-assistant message. |
| 400 | CHAT_RETRY_NO_USER_MESSAGE | A retry was requested but there is no user turn to retry from. |
| 404 | CHAT_RETRY_MESSAGE_NOT_FOUND | The `messageId` to retry was not found in the conversation. |
| 429 | RATE_LIMIT_TOO_MANY_REQUESTS | The per-key rate limit was exceeded. |
| 500 | INTERNAL_SERVER_ERROR | An unexpected server error. Safe to retry with backoff. |

## Rate limiting

API v2 is rate limited **per API key** using a sliding window of **100 requests per 10 seconds**. Every v2 response carries the current limit state in headers:

| Header | Meaning |
| --- | --- |
| X-RateLimit-Limit | The window ceiling (100). |
| X-RateLimit-Remaining | Requests left in the current window. |
| X-RateLimit-Reset | Unix-ms timestamp when the window resets. |

When the limit is exceeded you receive `429` with code `RATE_LIMIT_TOO_MANY_REQUESTS`. Back off (roughly one second) and retry.

> **TIP:** Treat `429` and `5xx` as retryable. Retry with exponential backoff and a little jitter; do not retry `4xx` validation or auth errors — fix the request instead.

## The v1 (Chatbase-compatible) error body

The Chatbase-parity contacts and custom-attributes endpoints under `/api/v1/chatbots/...` use the conventional status codes but a simpler body containing only a `message`:

```json
{ "message": "Resource not found" }
```

| Status | Example message | When |
| --- | --- | --- |
| 400 | Invalid request data | Missing or malformed body (e.g. no `users` array on create). |
| 401 | No API key provided. / Invalid API key. | Missing or invalid Bearer key. |
| 404 | Resource not found | Unknown chatbot/contact/attribute, or out of your workspace. |
| 409 | External ID or email already exists. | A uniqueness conflict on `external_id`. |

The legacy v1 chat stream (`POST /api/v1/chat`) reports validation and not-found errors as JSON `{ success: false, error: { code, message } }` before the SSE stream opens. See [Chat with an agent (v1)](/docs/api/v1/chat).
