A page is what a visitor sees at boltclaw.ai/<slug>.
{
"id": "3f8c1e42-9b7a-4d61-a0f5-2c9e1b7d4a83",
"slug": "acme",
"title": "Acme Supply Co.",
"bio": "Everything for the discerning coyote.",
"avatar_url": "https://.../page-assets/3f8c1e42-.../avatar.png",
"theme": { "background": "#111111", "accent": "#5B8DEF", "buttonStyle": "solid" },
"socials": [{ "platform": "instagram", "url": "https://example.com/social" }],
"contact_enabled": true,
"contact_note": "I reply within two days. Booking enquiries only, please.",
"official_channel": true,
"published": true,
"created_at": "2026-03-01T09:12:44.201118+00:00",
"updated_at": "2026-03-04T17:02:10.885210+00:00"
}
avatar_url is read-only here — avatars are uploaded from the dashboard.
Slugs
A slug is the public URL of the page, so it is fixed once created. There is no endpoint that renames one: a rename breaks every link, QR code and printed card pointing at the old name, and frees that name for someone else.
A slug must be lowercase letters, digits and single hyphens, 1–63 characters,
with no leading or trailing hyphen and no doubled hyphen. acme, acme-supply
and acme2 are fine; Acme, -acme, acme- and acme--supply are not.
These names are reserved and cannot be used:
admin ads api assets auth blog chatgpt-integration compare dashboard
docs faq favicon features onboarding portal pricing privacy public r
robots settings sitemap static support terms tools use-cases waitlist
why-boltclaw
Themes
theme accepts only these keys. An unrecognised key is rejected with 400
rather than silently dropped, so a typo is something you find at the request
and not on the rendered page:
background, surface, text, muted, accent, accentText, border,
font, buttonStyle, radius
Each value is a string of at most 64 characters. Colours may be hex
(#5B8DEF), a colour function (rgb(...), oklch(...)), or a CSS colour
keyword. A value the renderer does not recognise falls back to the default
rather than breaking the page.
Socials
socials is an array of at most 12 { "platform", "url" } objects (the
default on every plan; social_icons_per_page in GET /v1/usage). platform
is a short lowercase slug (instagram, x, github). url must be http,
https, mailto: or tel:. Any other key on an entry is rejected.
Operations
GET /v1/pages
List pages
Scope pages:read.
The workspace's pages, newest first.
Parameters
| Name | In | Type | |
|---|---|---|---|
limit |
query | integer |
Page size, 1 to 200. Above 200 is clamped; anything else is the default. (default 50; >= 1; <= 200) |
cursor |
query | string |
The next_cursor from the previous page. Opaque; an invalid one is 400. (max 512) |
Answers
| Status | Body | |
|---|---|---|
| 200 | PageList |
A page of pages. |
| 400 | ErrorEnvelope |
VALIDATION_ERROR: a body that is not JSON, a field that breaks its rule (named in fields), a bad query value or cursor, or a malformed Idempotency-Key. |
| 401 | ErrorEnvelope |
UNAUTHORIZED: no key, or one that is malformed, unknown or revoked. It never says which. |
| 403 | ErrorEnvelope |
INSUFFICIENT_SCOPE: the key lacks this operation's scope. Or ORGANISATION_SUSPENDED: the key's organisation is suspended. Never about another workspace's object, which is 404. |
| 429 | ErrorEnvelope |
RATE_LIMITED: too many requests. Wait for Retry-After seconds. |
| 500 | ErrorEnvelope |
INTERNAL_ERROR: ours. Safe to retry; send the same Idempotency-Key on a POST. |
POST /v1/pages
Create a page
Scope pages:write. Accepts Idempotency-Key. Can make something publicly visible.
Creates a page at boltclaw.ai/{slug}. Slugs are global: 409 CONFLICT means the name is
taken, by anyone. A reserved slug is 400. The organisation's plan sets how many pages it
may hold and publish (402 PLAN_LIMIT). This operation draws on its own budget of 10
attempts an hour per key (429).
Request body (required)
| Field | Type | |
|---|---|---|
slug |
Slug |
Lowercase letters, digits and single hyphens. Reserved names are refused. (required; min 1; max 63) |
title |
string | null |
120 characters by default; your organisation's number is page_title_chars in GET /usage. (max 120) |
bio |
string | null |
600 characters by default; your organisation's number is page_bio_chars in GET /usage. (max 600) |
theme |
Theme |
The page's colours and style. Each value is a string of 1 to 64 characters. |
socials |
Social[] |
12 icons by default; your organisation's number is social_icons_per_page in GET /usage. A page already holding more may save the same number or fewer. (<= 12 items) |
contact_enabled |
boolean |
|
contact_note |
string | null |
300 characters by default; your organisation's number is contact_note_chars in GET /usage. (max 300) |
official_channel |
boolean |
|
published |
boolean |
{
"slug": "<slug>"
}
Answers
| Status | Body | |
|---|---|---|
| 201 | Page |
Created. |
| 400 | ErrorEnvelope |
VALIDATION_ERROR: a body that is not JSON, a field that breaks its rule (named in fields), a bad query value or cursor, or a malformed Idempotency-Key. |
| 401 | ErrorEnvelope |
UNAUTHORIZED: no key, or one that is malformed, unknown or revoked. It never says which. |
| 402 | ErrorEnvelope |
PLAN_LIMIT: the organisation's plan does not allow more of this. The message names the allowance. |
| 403 | ErrorEnvelope |
INSUFFICIENT_SCOPE: the key lacks this operation's scope. Or ORGANISATION_SUSPENDED: the key's organisation is suspended. Never about another workspace's object, which is 404. |
| 409 | ErrorEnvelope |
CONFLICT: the request clashes with the object's current state (the message says how). IDEMPOTENCY_KEY_CONFLICT / IDEMPOTENCY_KEY_IN_FLIGHT: the Idempotency-Key was used with another request, or its first request is still running. |
| 413 | ErrorEnvelope |
PAYLOAD_TOO_LARGE: the body is over 64 KB. |
| 429 | ErrorEnvelope |
RATE_LIMITED: too many requests. Wait for Retry-After seconds. |
| 500 | ErrorEnvelope |
INTERNAL_ERROR: ours. Safe to retry; send the same Idempotency-Key on a POST. |
GET /v1/pages/{slug}
Read a page
Scope pages:read. Another workspace's answers 404.
One of the workspace's pages, published or not.
Parameters
| Name | In | Type | |
|---|---|---|---|
slug |
path | Slug |
Lowercase letters, digits and single hyphens. Reserved names are refused. (required; min 1; max 63) |
Answers
| Status | Body | |
|---|---|---|
| 200 | Page |
The page. |
| 401 | ErrorEnvelope |
UNAUTHORIZED: no key, or one that is malformed, unknown or revoked. It never says which. |
| 403 | ErrorEnvelope |
INSUFFICIENT_SCOPE: the key lacks this operation's scope. Or ORGANISATION_SUSPENDED: the key's organisation is suspended. Never about another workspace's object, which is 404. |
| 404 | ErrorEnvelope |
NOT_FOUND: no such object, or it belongs to another workspace. The two are deliberately the same answer and never 403. If the id is right, check you are using the right key. |
| 429 | ErrorEnvelope |
RATE_LIMITED: too many requests. Wait for Retry-After seconds. |
| 500 | ErrorEnvelope |
INTERNAL_ERROR: ours. Safe to retry; send the same Idempotency-Key on a POST. |
PATCH /v1/pages/{slug}
Update a page
Scope pages:write. Another workspace's answers 404. Can make something publicly visible.
Changes the fields you send. The slug cannot change: it is the page's public address, and
renaming it would break every link and QR code pointing at it. Publishing a page counts
against the plan's published-page allowance (402 PLAN_LIMIT).
Parameters
| Name | In | Type | |
|---|---|---|---|
slug |
path | Slug |
Lowercase letters, digits and single hyphens. Reserved names are refused. (required; min 1; max 63) |
Request body (required)
| Field | Type | |
|---|---|---|
title |
string | null |
120 characters by default; your organisation's number is page_title_chars in GET /usage. (max 120) |
bio |
string | null |
600 characters by default; your organisation's number is page_bio_chars in GET /usage. (max 600) |
theme |
Theme |
The page's colours and style. Each value is a string of 1 to 64 characters. |
socials |
Social[] |
12 icons by default; your organisation's number is social_icons_per_page in GET /usage. A page already holding more may save the same number or fewer. (<= 12 items) |
contact_enabled |
boolean |
|
contact_note |
string | null |
300 characters by default; your organisation's number is contact_note_chars in GET /usage. (max 300) |
official_channel |
boolean |
|
published |
boolean |
Answers
| Status | Body | |
|---|---|---|
| 200 | Page |
The updated page. |
| 400 | ErrorEnvelope |
VALIDATION_ERROR: a body that is not JSON, a field that breaks its rule (named in fields), a bad query value or cursor, or a malformed Idempotency-Key. |
| 401 | ErrorEnvelope |
UNAUTHORIZED: no key, or one that is malformed, unknown or revoked. It never says which. |
| 402 | ErrorEnvelope |
PLAN_LIMIT: the organisation's plan does not allow more of this. The message names the allowance. |
| 403 | ErrorEnvelope |
INSUFFICIENT_SCOPE: the key lacks this operation's scope. Or ORGANISATION_SUSPENDED: the key's organisation is suspended. Never about another workspace's object, which is 404. |
| 404 | ErrorEnvelope |
NOT_FOUND: no such object, or it belongs to another workspace. The two are deliberately the same answer and never 403. If the id is right, check you are using the right key. |
| 413 | ErrorEnvelope |
PAYLOAD_TOO_LARGE: the body is over 64 KB. |
| 428 | ErrorEnvelope |
APPROVAL_REQUIRED on the first try, APPROVAL_PENDING on a retry before a person has decided. NOTHING HAS HAPPENED either way. The body carries approval with a url: show it to the person, and once they approve, send the identical request again with the approval id in BoltClaw-Approval. Only an AI assistant's request is ever answered this way. |
| 429 | ErrorEnvelope |
RATE_LIMITED: too many requests. Wait for Retry-After seconds. |
| 500 | ErrorEnvelope |
INTERNAL_ERROR: ours. Safe to retry; send the same Idempotency-Key on a POST. |
DELETE /v1/pages/{slug}
Delete a page
Scope pages:delete. Another workspace's answers 404.
Deletes the page, its links and its statistics. People who messaged it keep a read-only
copy of their own conversation; your access to those conversations ends. Needs its own
scope, pages:delete, which pages:write does not include.
Parameters
| Name | In | Type | |
|---|---|---|---|
slug |
path | Slug |
Lowercase letters, digits and single hyphens. Reserved names are refused. (required; min 1; max 63) |
Answers
| Status | Body | |
|---|---|---|
| 200 | PageDeleted |
Deleted. |
| 401 | ErrorEnvelope |
UNAUTHORIZED: no key, or one that is malformed, unknown or revoked. It never says which. |
| 403 | ErrorEnvelope |
INSUFFICIENT_SCOPE: the key lacks this operation's scope. Or ORGANISATION_SUSPENDED: the key's organisation is suspended. Never about another workspace's object, which is 404. |
| 404 | ErrorEnvelope |
NOT_FOUND: no such object, or it belongs to another workspace. The two are deliberately the same answer and never 403. If the id is right, check you are using the right key. |
| 428 | ErrorEnvelope |
APPROVAL_REQUIRED on the first try, APPROVAL_PENDING on a retry before a person has decided. NOTHING HAS HAPPENED either way. The body carries approval with a url: show it to the person, and once they approve, send the identical request again with the approval id in BoltClaw-Approval. Only an AI assistant's request is ever answered this way. |
| 429 | ErrorEnvelope |
RATE_LIMITED: too many requests. Wait for Retry-After seconds. |
| 500 | ErrorEnvelope |
INTERNAL_ERROR: ours. Safe to retry; send the same Idempotency-Key on a POST. |
Delete a page — answers 200
The page, its links and its statistics, and your access to every conversation on it. There is no undo. The other person keeps a read-only archive of their own thread.
curl -X DELETE https://api.boltclaw.ai/v1/pages/acme \
-H "Authorization: Bearer bc_live_EXAMPLExxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
const res = await fetch("https://api.boltclaw.ai/v1/pages/acme", {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.BOLTCLAW_KEY}`,
},
});
// 200
const data = await res.json();
GET /v1/pages/{slug}/stats
Read a page's views and clicks
Scope pages:read. Another workspace's answers 404.
Views and link clicks between from and to (the last 30 days by default), counted by
BoltClaw itself. The answer starts no earlier than your plan's analytics history allows
(analytics_days in GET /usage: 30 days on Free), whatever from says; the window
actually used is echoed back, so a from later than the one you sent means the plan's
history ended there. Nothing older is deleted: a larger plan sees it at once.
Parameters
| Name | In | Type | |
|---|---|---|---|
slug |
path | Slug |
Lowercase letters, digits and single hyphens. Reserved names are refused. (required; min 1; max 63) |
from |
query | string |
An ISO-8601 date or timestamp. Default 30 days before to. |
to |
query | string |
An ISO-8601 date or timestamp, later than from. Default now. |
Answers
| Status | Body | |
|---|---|---|
| 200 | PageStats |
The counts. |
| 400 | ErrorEnvelope |
VALIDATION_ERROR: a body that is not JSON, a field that breaks its rule (named in fields), a bad query value or cursor, or a malformed Idempotency-Key. |
| 401 | ErrorEnvelope |
UNAUTHORIZED: no key, or one that is malformed, unknown or revoked. It never says which. |
| 403 | ErrorEnvelope |
INSUFFICIENT_SCOPE: the key lacks this operation's scope. Or ORGANISATION_SUSPENDED: the key's organisation is suspended. Never about another workspace's object, which is 404. |
| 404 | ErrorEnvelope |
NOT_FOUND: no such object, or it belongs to another workspace. The two are deliberately the same answer and never 403. If the id is right, check you are using the right key. |
| 429 | ErrorEnvelope |
RATE_LIMITED: too many requests. Wait for Retry-After seconds. |
| 500 | ErrorEnvelope |
INTERNAL_ERROR: ours. Safe to retry; send the same Idempotency-Key on a POST. |
Shapes
The objects these operations send and answer.
NextCursor
Pass back as cursor for the next page; null on the last page.
string | null.
Page
| Field | Type | |
|---|---|---|
id |
uuid |
(required) |
slug |
Slug |
Lowercase letters, digits and single hyphens. Reserved names are refused. (required; min 1; max 63) |
title |
string | null |
(required) |
bio |
string | null |
(required) |
avatar_url |
uri | null |
(required) |
theme |
Theme |
The page's colours and style. Each value is a string of 1 to 64 characters. (required) |
socials |
Social[] |
(required) |
contact_enabled |
boolean |
(required) |
contact_note |
string | null |
The creator's own words above the message box. (required) |
official_channel |
boolean |
The creator's promise that this is the only channel they answer through. (required) |
published |
boolean |
(required) |
created_at |
date-time |
(required) |
updated_at |
date-time |
(required) |
PageCreate
| Field | Type | |
|---|---|---|
slug |
Slug |
Lowercase letters, digits and single hyphens. Reserved names are refused. (required; min 1; max 63) |
title |
string | null |
120 characters by default; your organisation's number is page_title_chars in GET /usage. (max 120) |
bio |
string | null |
600 characters by default; your organisation's number is page_bio_chars in GET /usage. (max 600) |
theme |
Theme |
The page's colours and style. Each value is a string of 1 to 64 characters. |
socials |
Social[] |
12 icons by default; your organisation's number is social_icons_per_page in GET /usage. A page already holding more may save the same number or fewer. (<= 12 items) |
contact_enabled |
boolean |
|
contact_note |
string | null |
300 characters by default; your organisation's number is contact_note_chars in GET /usage. (max 300) |
official_channel |
boolean |
|
published |
boolean |
PageDeleted
| Field | Type | |
|---|---|---|
deleted |
true |
(required) |
slug |
Slug |
Lowercase letters, digits and single hyphens. Reserved names are refused. (required; min 1; max 63) |
message |
string |
(required) |
PageList
| Field | Type | |
|---|---|---|
data |
Page[] |
(required) |
next_cursor |
NextCursor |
Pass back as cursor for the next page; null on the last page. (required) |
PageStats
| Field | Type | |
|---|---|---|
slug |
Slug |
Lowercase letters, digits and single hyphens. Reserved names are refused. (required; min 1; max 63) |
from |
date-time |
(required) |
to |
date-time |
(required) |
views |
integer |
(required) |
clicks |
integer |
(required) |
links |
object[] |
(required) |
PageUpdate
At least one field. The slug cannot change.
| Field | Type | |
|---|---|---|
title |
string | null |
120 characters by default; your organisation's number is page_title_chars in GET /usage. (max 120) |
bio |
string | null |
600 characters by default; your organisation's number is page_bio_chars in GET /usage. (max 600) |
theme |
Theme |
The page's colours and style. Each value is a string of 1 to 64 characters. |
socials |
Social[] |
12 icons by default; your organisation's number is social_icons_per_page in GET /usage. A page already holding more may save the same number or fewer. (<= 12 items) |
contact_enabled |
boolean |
|
contact_note |
string | null |
300 characters by default; your organisation's number is contact_note_chars in GET /usage. (max 300) |
official_channel |
boolean |
|
published |
boolean |
Slug
Lowercase letters, digits and single hyphens. Reserved names are refused.
string.
Social
| Field | Type | |
|---|---|---|
platform |
string |
(required) |
url |
string |
(required; max 2048) |
Theme
The page's colours and style. Each value is a string of 1 to 64 characters.
| Field | Type | |
|---|---|---|
background |
string |
(min 1; max 64) |
surface |
string |
(min 1; max 64) |
text |
string |
(min 1; max 64) |
muted |
string |
(min 1; max 64) |
accent |
string |
(min 1; max 64) |
accentText |
string |
(min 1; max 64) |
border |
string |
(min 1; max 64) |
font |
string |
(min 1; max 64) |
buttonStyle |
string |
(min 1; max 64) |
radius |
string |
(min 1; max 64) |