Every error is JSON with the same fields:
{ "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:
{ "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
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.
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.
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.
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.
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.
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.
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.
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.
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.
ALREADY_EXISTS
409 — Reserved; not answered today.
Reserved; nothing answers it today. Treat it like CONFLICT.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
curl https://api.boltclaw.ai/v1/ping \
-H "Authorization: Bearer bc_live_notakeynotakeynotakeynotakeynotakeynot"
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.
curl -X POST https://api.boltclaw.ai/v1/pages \
-H "Authorization: Bearer bc_live_EXAMPLExxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"slug": "Acme Supply"
}'
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.
curl https://api.boltclaw.ai/v1/pages/acme-does-not-exist \
-H "Authorization: Bearer bc_live_EXAMPLExxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
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. Between them a collision probe is expensive and slug squatting is
capped.