# Errors

Every code, what it means, and what to change. Branch on `code`, never on the sentence.

Every error is JSON with the same fields:

```json
{ "error": "That slug is reserved and cannot be used", "code": "VALIDATION_ERROR", "request_id": "0b9e4c1a-7d2f-4e8b-9a31-5c6d7e8f9a0b" }
```

Match on `code`, not on `error` — the sentence may be reworded, the code will
not. `request_id` identifies the request: quote it if you contact us. A
validation error can also carry `fields`, each field the request got wrong and
the rule it broke:

```json
{ "error": "Invalid request: closed: Expected boolean, received string", "code": "VALIDATION_ERROR", "request_id": "…", "fields": { "closed": "Expected boolean, received string" } }
```

| `code` | Status | What it means |
|---|---|---|
| `UNAUTHORIZED` | 401 | No key, a malformed one, or one that does not exist or was revoked. The answer never says which. |
| `FORBIDDEN` | 403 | Refused for a reason no scope would change. The message says what to do. |
| `NOT_FOUND` | 404 | No such object, or another workspace's. Deliberately indistinguishable. Also an unknown path. |
| `VALIDATION_ERROR` | 400, 422 | The request or a value in it is not acceptable. `fields` names each field where it can. |
| `CONFLICT` | 409 | The request clashes with the object's current state: a taken slug, a closed conversation, a full page. |
| `RATE_LIMITED` | 429 | Too many requests. Wait for `Retry-After`. |
| `METHOD_NOT_ALLOWED` | 405 | The path exists; the method does not. `Allow` lists the ones that do. |
| `PAYLOAD_TOO_LARGE` | 413 | The body is over 64 KB. |
| `INTERNAL_ERROR` | 500, 503 | Ours. Safe to retry, with the same `Idempotency-Key`. |
| `ALREADY_EXISTS` | 409 | Reserved; not answered today. |
| `PLAN_LIMIT` | 402 | The organisation's plan does not allow more of this. The message names the allowance. |
| `INSUFFICIENT_SCOPE` | 403 | The key is valid; it was not issued for this operation. |
| `ORGANISATION_SUSPENDED` | 403 | The key's organisation is suspended. Its published pages stay live. |
| `UPSTREAM_ERROR` | 422, 502, 503 | A social network refused or failed the call. The message says what happened. |
| `UNAVAILABLE` | 503 | Dashboard only; `/v1` answers `500 INTERNAL_ERROR` instead. |
| `THREAD_ALREADY_OPEN` | 409 | Dashboard only: the person already has an open conversation on that page. |
| `MFA_REQUIRED` | 401 | Dashboard only: the session owes its second sign-in step. |
| `REAUTH_REQUIRED` | 401 | Dashboard only: changing where money goes needs a sign-in in the last 15 minutes. |
| `TWO_STEP_REQUIRED` | 403 | Dashboard only: sending payouts or connecting a payout account needs two-step verification. |
| `IDEMPOTENCY_KEY_CONFLICT` | 409 | The `Idempotency-Key` was already used with a different request. |
| `IDEMPOTENCY_KEY_IN_FLIGHT` | 409 | The first request with this `Idempotency-Key` is still running. |
| `AGENT_ACCESS_DISABLED` | 403 | An AI assistant's key, in an organisation that has turned assistants off. |
| `APPROVAL_REQUIRED` | 428 | A person must approve this AI assistant's request first. Nothing has happened. |
| `APPROVAL_PENDING` | 428 | The approval has not been decided yet. Nothing has happened. |
| `APPROVAL_DENIED` | 403 | A person declined the request. Nothing has happened. |
| `APPROVAL_INVALID` | 409 | The approval is spent, has lapsed, or is for another request or another key. |

## What to do about each code

<a id="UNAUTHORIZED"></a>

### UNAUTHORIZED

**401** — No key, a malformed one, or one that does not exist or was revoked. The answer never says which.

Check the key is present, complete, and sent as `Authorization: Bearer bc_live_...`. This answer never says whether the key is missing, mistyped, unknown or revoked, so you cannot tell from it which mistake you made. A revoked key never works again: create a new one under Settings, Developers.

<a id="FORBIDDEN"></a>

### FORBIDDEN

**403** — Refused for a reason no scope would change. The message says what to do.

Read the message: it names the reason and what to do instead, and no scope or retry changes it.

<a id="NOT_FOUND"></a>

### NOT_FOUND

**404** — No such object, or another workspace's. Deliberately indistinguishable. Also an unknown path.

Either there is no such object, or it belongs to another workspace; the two are the same answer on purpose. If you are sure of the id or slug, check you are using the key for the right workspace (`GET /workspace`). An unknown path answers this too.

<a id="VALIDATION_ERROR"></a>

### VALIDATION_ERROR

**400, 422** — The request or a value in it is not acceptable. `fields` names each field where it can.

Fix the request before retrying. `fields` names each field and the rule it broke, where there is one. Common causes: a body that is not JSON, a field the operation does not take (bodies are strict), a value out of range, or a cursor you built rather than passed back. A 422 means the request was well formed but cannot be done as asked, such as an action the network does not support.

<a id="CONFLICT"></a>

### CONFLICT

**409** — The request clashes with the object's current state: a taken slug, a closed conversation, a full page.

The request clashes with the object's current state, and the message says how: the slug is taken, the conversation is closed, the page already holds 500 links, the post is no longer a draft, or a payout's total changed. Do what it says, or re-read the object and retry.

<a id="RATE_LIMITED"></a>

### RATE_LIMITED

**429** — Too many requests. Wait for `Retry-After`.

Wait for the number of seconds in `Retry-After`, then retry. Watch `X-RateLimit-Remaining` so you slow down before you are refused. `POST /pages` and `POST /inquiries/{id}/messages` have smaller hourly budgets of their own, and a single conversation takes at most 30 messages an hour.

<a id="METHOD_NOT_ALLOWED"></a>

### METHOD_NOT_ALLOWED

**405** — The path exists; the method does not. `Allow` lists the ones that do.

The path exists but not with this method. The `Allow` header lists the methods it takes. There is no PUT: send PATCH with only the fields you are changing.

<a id="PAYLOAD_TOO_LARGE"></a>

### PAYLOAD_TOO_LARGE

**413** — The body is over 64 KB.

The body is over 64 KB. Nothing in this API needs a body that large: send less, or upload a file through `POST /media` instead of inlining it.

<a id="INTERNAL_ERROR"></a>

### INTERNAL_ERROR

**500, 503** — Ours. Safe to retry, with the same `Idempotency-Key`.

Ours, not yours. Safe to retry, and safer with the same `Idempotency-Key` on a POST, which guarantees a request that did succeed is not repeated. If it keeps happening, quote the `request_id` to support.

<a id="ALREADY_EXISTS"></a>

### ALREADY_EXISTS

**409** — Reserved; not answered today.

Reserved; nothing answers it today. Treat it like CONFLICT.

<a id="PLAN_LIMIT"></a>

### PLAN_LIMIT

**402** — The organisation's plan does not allow more of this. The message names the allowance.

The organisation's plan does not allow more of this, and the message names the allowance and the number. Delete something it counts, wait for the monthly allowance to reset (`GET /usage` shows where it stands), or upgrade the plan in the dashboard. Retrying will not help.

<a id="INSUFFICIENT_SCOPE"></a>

### INSUFFICIENT_SCOPE

**403** — The key is valid; it was not issued for this operation.

The key is valid but was not issued for this operation; the message names the scope it lacks. Create a key with that scope under Settings, Developers (`GET /ping` lists what this one carries). It never means the object belongs to someone else.

<a id="ORGANISATION_SUSPENDED"></a>

### ORGANISATION_SUSPENDED

**403** — The key's organisation is suspended. Its published pages stay live.

BoltClaw has suspended the key's organisation, so its keys are refused. Its published pages stay live. The organisation's owner can contact BoltClaw support; nothing you send changes it.

<a id="UPSTREAM_ERROR"></a>

### UPSTREAM_ERROR

**422, 502, 503** — A social network refused or failed the call. The message says what happened.

The social network refused or failed the call, and the message says what happened. A 503 is temporary: retry later. A 422 is the network refusing this content or action: change it. A 502 means the network answered in a way we could not use: retry once, then check the account is still connected (`GET /accounts`).

<a id="UNAVAILABLE"></a>

### UNAVAILABLE

**503** — Dashboard only; `/v1` answers `500 INTERNAL_ERROR` instead.

The dashboard answers this when a service it needs is briefly unavailable; the API answers INTERNAL_ERROR instead. Retry in a little while.

<a id="THREAD_ALREADY_OPEN"></a>

### THREAD_ALREADY_OPEN

**409** — Dashboard only: the person already has an open conversation on that page.

The dashboard answers this when the person already has an open conversation on that page: continue it rather than starting another.

<a id="MFA_REQUIRED"></a>

### MFA_REQUIRED

**401** — Dashboard only: the session owes its second sign-in step.

The dashboard answers this when a signed-in session still owes its second step. Enter the code from the authenticator app, then retry.

<a id="REAUTH_REQUIRED"></a>

### REAUTH_REQUIRED

**401** — Dashboard only: changing where money goes needs a sign-in in the last 15 minutes.

The dashboard answers this when a change of where money goes (payout details, unlocking payouts, a payout or billing credential) comes from a session that signed in more than 15 minutes ago. Sign in again, then retry. The API never answers it: a key has no session.

<a id="TWO_STEP_REQUIRED"></a>

### TWO_STEP_REQUIRED

**403** — Dashboard only: sending payouts or connecting a payout account needs two-step verification.

The dashboard answers this when sending payouts, or connecting a payout account, comes from a session without two-step verification. Turn two-step on in Settings, Account, sign in with the code, then retry. The API never answers it: a key has no session, and an AI assistant's send is approved by a person in a two-step session instead.

<a id="IDEMPOTENCY_KEY_CONFLICT"></a>

### IDEMPOTENCY_KEY_CONFLICT

**409** — The `Idempotency-Key` was already used with a different request.

The `Idempotency-Key` was already used with a different request. That is a bug in the client rather than a retry: generate one key per logical operation, and reuse it only to repeat that same request.

<a id="IDEMPOTENCY_KEY_IN_FLIGHT"></a>

### IDEMPOTENCY_KEY_IN_FLIGHT

**409** — The first request with this `Idempotency-Key` is still running.

The first request with this `Idempotency-Key` is still running. Wait a moment and retry with the same key; you will get its answer replayed.

<a id="AGENT_ACCESS_DISABLED"></a>

### AGENT_ACCESS_DISABLED

**403** — An AI assistant's key, in an organisation that has turned assistants off.

An owner or admin has turned AI assistant access off for this organisation, under Settings, AI agents. Only a key approved for an assistant is refused this way; a key a person created in the dashboard is unaffected. Nothing you send changes it: a person has to turn it back on.

<a id="APPROVAL_REQUIRED"></a>

### APPROVAL_REQUIRED

**428** — A person must approve this AI assistant's request first. Nothing has happened.

NOTHING HAS HAPPENED. A person must approve this action first. Show them the `url` in the `approval` object the body carries; once they approve, send the identical request again with the approval id in the `BoltClaw-Approval` header. The approval expires after 24 hours and can be spent once.

<a id="APPROVAL_PENDING"></a>

### APPROVAL_PENDING

**428** — The approval has not been decided yet. Nothing has happened.

NOTHING HAS HAPPENED. The person has not decided yet. Wait, then send the identical request again with the same approval id. `GET /v1/approvals/{id}` tells you where it has got to without making the request again.

<a id="APPROVAL_DENIED"></a>

### APPROVAL_DENIED

**403** — A person declined the request. Nothing has happened.

A person said no, and nothing happened. Do not try it again unless they ask you to.

<a id="APPROVAL_INVALID"></a>

### APPROVAL_INVALID

**409** — The approval is spent, has lapsed, or is for another request or another key.

The approval is spent, has lapsed, was for a different request, or belongs to another key. Send the request again WITHOUT the header to ask for a new one. If the message says what the request would do has changed, something about the object moved after the person approved it: look again and ask again.

**A key that is not a key** — answers `401`

`401` for a missing key, a malformed one, an unknown one and a revoked one alike. The answer never says which, so nothing can be probed.

```bash
curl https://api.boltclaw.ai/v1/ping \
  -H "Authorization: Bearer bc_live_notakeynotakeynotakeynotakeynotakeynot"
```

```js
const res = await fetch("https://api.boltclaw.ai/v1/ping", {
  headers: {
    Authorization: `Bearer ${process.env.BOLTCLAW_KEY}`,
  },
});

// 401: branch on the "code", not the sentence
const data = await res.json();
```

**A value the API will not take** — answers `400`

`fields` names each field and the rule it broke, so a client can show the message beside the input rather than as an alert.

```bash
curl -X POST https://api.boltclaw.ai/v1/pages \
  -H "Authorization: Bearer bc_live_EXAMPLExxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
  "slug": "Acme Supply"
}'
```

```js
const res = await fetch("https://api.boltclaw.ai/v1/pages", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.BOLTCLAW_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "slug": "Acme Supply"
  }),
});

// 400: branch on the "code", not the sentence
const data = await res.json();
```

## A note on 404

A page that belongs to a different account returns **404, not 403**. There is
no response anywhere in this API that distinguishes "this does not exist" from
"this is not yours" — that distinction would let anyone with a key discover
which page names are taken, and pages are deliberately not discoverable.

The `403` above is not an exception to this. It is decided from your key alone,
before any slug or id is looked at, so it says nothing about whether a page or
thread exists — only what your own credential may do, which you already know.
No `403` in this API is ever a statement about a row.

**A page that is not yours** — answers `404`

Indistinguishable from one that does not exist. That is the whole point: a `403` here would confirm the slug is taken.

```bash
curl https://api.boltclaw.ai/v1/pages/acme-does-not-exist \
  -H "Authorization: Bearer bc_live_EXAMPLExxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

```js
const res = await fetch("https://api.boltclaw.ai/v1/pages/acme-does-not-exist", {
  headers: {
    Authorization: `Bearer ${process.env.BOLTCLAW_KEY}`,
  },
});

// 404: branch on the "code", not the sentence
const data = await res.json();
```

**Creating a page is the exception, and it is a real one.** `POST /v1/pages`
with a slug somebody else holds returns `409`, which confirms the name is
taken. It cannot do otherwise — the alternative is a create that silently does
nothing — but we would rather write down what it gives away than claim it gives
away nothing. The response says only that the name is unavailable: never whose
it is, never whether it is published, never anything about the page.

Two things bound it. `POST /v1/pages` draws on a separate, much smaller rate
limit — **10 attempts per hour, per key** — rather than the per-minute
allowance the rest of the API gets, and an account may hold only as many pages as
[its plan allows](https://boltclaw.ai/docs/rate-limits#pages-per-account). Between them a collision probe is expensive and slug squatting is
capped.

<!-- https://boltclaw.ai/docs/errors as markdown. Generated from openapi.yaml; API version 1.22.0. -->
<!-- Every /docs page answers as markdown: add .md to its path, or send Accept: text/markdown. -->
