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

# Errors

> Every Cactal API error uses one JSON shape: a stable kind, a human-readable message, and optional details. Statuses, causes, and how to recover.

Every non-2xx response from the Cactal API — and every failed MCP tool call — carries the same body shape:

```json theme={null}
{
  "kind": "not_found",
  "message": "Website not found"
}
```

* `kind` — a stable, machine-readable category. Branch on this, never on `message`.
* `message` — a human-readable explanation. Wording may change.
* `suggestion`: present when the failure has a known fix: what to do next, often naming the exact operation to call (for example a stale edit lease points at `websiteSourceCode.lease.acquire`).
* `validValues`: present when the failing field accepts a closed set of values: the accepted values.
* Some errors add further detail fields alongside these (for example `retryAfterSeconds` on rate limits, or framework diagnostics on failed builds).

## Error kinds

| `kind`         | Status | Retry?                       |
| -------------- | ------ | ---------------------------- |
| `validation`   | `400`  | After fixing the request     |
| `unauthorized` | `401`  | After fixing credentials     |
| `forbidden`    | `403`  | After changing key scope     |
| `not_found`    | `404`  | No                           |
| `conflict`     | `409`  | After resolving the conflict |
| `rate_limited` | `429`  | After `Retry-After` seconds  |
| `internal`     | `500`  | Yes, with backoff            |

## validation

The request was well-formed HTTP but failed schema validation. The message names the first invalid field, and closed-set fields include their accepted values in `validValues`.

```json theme={null}
{
  "kind": "validation",
  "message": "unit: Invalid option: expected one of \"year\"|\"month\"|\"day\"|\"hour\"|\"minute\"",
  "validValues": ["year", "month", "day", "hour", "minute"]
}
```

**Suggested action:** fix the named field and resend. Validation failures are never retryable as-is.

<Note>
  Framework build failures also surface as `validation` errors — on `head/check`, `head/publish`, and `deployment/ensure` — with diagnostic details describing each source error. Fix the source files they point at, then retry.
</Note>

## unauthorized

No usable credential. The key is missing, malformed, expired, or revoked — or you sent a browser session cookie to `api.cactal.ai`.

```json theme={null}
{
  "kind": "unauthorized",
  "message": "Invalid API key"
}
```

**Suggested action:** send `Authorization: Bearer <key>` with an active key. If the key was rotated, update the stored secret. Repeated auth failures from one IP are rate limited — see [Rate limits](/docs/api-reference/rate-limits).

## forbidden

The key authenticated, but its role does not grant the capability this endpoint requires (shown as `x-cactal-required-capability` on each endpoint page).

```json theme={null}
{
  "kind": "forbidden",
  "message": "Missing capability websites.create"
}
```

**Suggested action:** use a key whose scope and role cover the operation, or rescope the key by sending `access` to `PATCH /v1/apiKeys/{apiKeyId}`. Do not retry with the same key.

## not\_found

The resource does not exist — or it exists but is outside your key's access scope. Cactal deliberately returns `404` instead of `403` for cross-tenant requests so resource existence never leaks between organizations.

```json theme={null}
{
  "kind": "not_found",
  "message": "Website not found"
}
```

**Suggested action:** check the id and confirm the key's scope includes the resource's organization or website. An agent seeing `404` on an id it just received should stop and re-check its scope rather than retry.

## conflict

The request contradicts current state: a duplicate slug or field key, a resource cap reached, or a delete blocked by incoming references. The message states the specific conflict.

```json theme={null}
{
  "kind": "conflict",
  "message": "An item with this slug already exists"
}
```

**Suggested action:** resolve the stated conflict (pick a new slug, remove references, raise capacity) and resend.

## rate\_limited

You exceeded a rate-limit bucket. The response includes a `Retry-After` header (seconds) and mirrors it in the body:

```json theme={null}
{
  "kind": "rate_limited",
  "message": "Rate limit exceeded",
  "retryAfterSeconds": 2
}
```

**Suggested action:** wait `retryAfterSeconds`, then retry. Sustained 429s mean you should queue and pace requests — see [Rate limits](/docs/api-reference/rate-limits) for the exact buckets.

## internal

Something failed on Cactal's side. The request may or may not have taken effect.

```json theme={null}
{
  "kind": "internal",
  "message": "Internal error"
}
```

**Suggested action:** retry with exponential backoff. For non-idempotent operations (marked `x-cactal-idempotent: false`), read the resource first to confirm whether the change landed before retrying.

## Errors over MCP

MCP tool calls run the identical pipeline. Failures come back as tool results with `isError: true` and the same `{ kind, message, ... }` payload as JSON content, so agents can branch on `kind` exactly like HTTP clients do.
