# Errors & limits

## HTTP status codes

| Status | Typical cause |
|---|---|
| **401** | Missing, invalid, or expired API key |
| **403** | Wrong organisation, missing permission, source not allowed, widget rejected the request, or conversation does not belong to this visitor |
| **404** | Unknown organisation, source, page, widget, or conversation |
| **402** | No answer credits left (chat and summarize) |
| **422** | Invalid input, identity check failed, or keep limit reached |
| **429** | Too many requests |

### Invalid input

When a required field is missing or invalid, the body looks like this:

```json
{
  "message": "The content field is required.",
  "errors": {
    "content": ["The content field is required."]
  }
}
```

`message` is a human-readable summary. `errors` maps each field name to one or more messages.

## Rate limits

Limits are counted per minute.

### Ingestion API

Counted per API key (or IP if there is no key).

| Area | Default |
|---|---|
| Page push and related routes | 120 requests per minute |

### Widget API

Each group has a limit for the whole widget and a lower limit for a single visitor (widget + IP), so one visitor cannot use up the entire budget.

| Group | Endpoints | Defaults (widget / visitor) |
|---|---|---|
| Search | search | 60 / 20 |
| AI | chat, summarize | 20 / 6 |
| Events | events | 120 / 30 |
| Config | config, conversations | 300 / 60 |

Exceeding a limit returns **429**.

## Answers and billing

- Search and page ingestion do not use answer credits.
- Chat and summarize use your plan's monthly answers.
- When answers are used up, those endpoints return **402**.
- **Chat** attaches free search hits in `data` (same hit shape as the search endpoint) so the official embed can show results without a second request. Example:

```json
{
  "error": "answers_exhausted",
  "message": "AI answers are paused for this site — search still works.",
  "fallback": "search",
  "data": [
    { "id": "…", "external_id": null, "name": "…", "url": "…", "snippet": "…", "score": 0.5, "meta": null }
  ],
  "session_id": "…"
}
```

- **Summarize** returns the same error fields without a result list.

## Tips for reliable page sync

- Always send a stable `external_id` so retries update the same page instead of creating duplicates.
- Re-pushing identical content for an already indexed page is skipped.
- Batch requests accept at most **50** pages at a time.
