{
"id": "9d2b7c15-4e3a-4b88-9f21-0a6c5d3e7b90",
"label": "Our catalogue",
"url": "https://example.com/catalogue",
"icon": "book",
"position": 0,
"visible": true,
"created_at": "2026-03-01T09:14:02.554311+00:00",
"updated_at": "2026-03-01T09:14:02.554311+00:00"
}
position is the display order, ascending. It does not have to be unique or
contiguous; ties break by id, so leaving everything at 0 gives a stable but
arbitrary order. url must be http, https, mailto: or tel:.
Links are unlimited in practice. A page holds at most 1,000 links, here and
in the dashboard alike: a safety ceiling the same on every plan, which keeps a
page fast to open on a phone (links_per_page in GET /v1/usage). A page that
already had more when its ceiling was lowered keeps every one of them: they are
all listed, and each can still be edited, moved or removed. Nothing is added to
it until it is under the ceiling again.
Operations
GET /v1/pages/{slug}/links
List a page's links
Scope pages:read. Another workspace's answers 404.
Every link on the page, in display order, with no cursor. Links are unlimited in practice:
a page holds at most 1,000 links, a safety ceiling the same on every plan
(links_per_page in GET /usage). A page that already had more when its ceiling was
lowered keeps every one of them, and all of them are listed here.
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 | LinkList |
The links. |
| 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. |
Every link on a page, in display order — answers 200
One response, never paged. The cap is enforced on the write, not only on the read, so there is no link you could create and then not see here.
curl https://api.boltclaw.ai/v1/pages/acme/links \
-H "Authorization: Bearer bc_live_EXAMPLExxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
const res = await fetch("https://api.boltclaw.ai/v1/pages/acme/links", {
headers: {
Authorization: `Bearer ${process.env.BOLTCLAW_KEY}`,
},
});
// 200
const data = await res.json();
POST /v1/pages/{slug}/links
Add a link
Scope pages:write. Accepts Idempotency-Key. Another workspace's answers 404. Can make something publicly visible.
Adds a link to the page. A page holds at most 1,000 links, a safety ceiling the same on
every plan (links_per_page in GET /usage): adding one to a page at its ceiling is
409 CONFLICT. A page that had more when its ceiling was lowered keeps them all, and you can
still edit, reorder and remove them, but nothing is added to it until it is under the
ceiling. A label longer than your organisation's link_label_chars (120 by default) is
400.
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 | |
|---|---|---|
label |
string | null |
120 characters by default; your organisation's number is link_label_chars in GET /usage. (max 120) |
url |
string |
(required; min 1; max 2048) |
icon |
string | null |
(max 32) |
position |
integer |
(>= 0; <= 10000) |
visible |
boolean |
{
"url": "<url>"
}
Answers
| Status | Body | |
|---|---|---|
| 201 | Link |
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. |
| 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. |
| 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. |
PATCH /v1/links/{id}
Update a link
Scope pages:write. Another workspace's answers 404. Can make something publicly visible.
Changes the fields you send.
Parameters
| Name | In | Type | |
|---|---|---|---|
id |
path | uuid |
An id this API gave you. Anything that is not one of yours is 404. (required) |
Request body (required)
| Field | Type | |
|---|---|---|
label |
string | null |
120 characters by default; your organisation's number is link_label_chars in GET /usage. (max 120) |
url |
string |
(min 1; max 2048) |
icon |
string | null |
(max 32) |
position |
integer |
(>= 0; <= 10000) |
visible |
boolean |
Answers
| Status | Body | |
|---|---|---|
| 200 | Link |
The updated link. |
| 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. |
| 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. |
Move a link — answers 200
position is ascending and ties break by id, so a page whose links all sit at 0 has a stable but arbitrary order.
curl -X PATCH https://api.boltclaw.ai/v1/links/9d2b7c15-4e3a-4b88-9f21-0a6c5d3e7b90 \
-H "Authorization: Bearer bc_live_EXAMPLExxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"position": 1
}'
const res = await fetch("https://api.boltclaw.ai/v1/links/9d2b7c15-4e3a-4b88-9f21-0a6c5d3e7b90", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.BOLTCLAW_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"position": 1
}),
});
// 200
const data = await res.json();
DELETE /v1/links/{id}
Remove a link
Scope pages:write. Another workspace's answers 404.
Removes the link from its page. Ordinary page editing, so pages:write is enough.
Parameters
| Name | In | Type | |
|---|---|---|---|
id |
path | uuid |
An id this API gave you. Anything that is not one of yours is 404. (required) |
Answers
| Status | Body | |
|---|---|---|
| 200 | Deleted |
Removed. |
| 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. |
Shapes
The objects these operations send and answer.
Deleted
| Field | Type | |
|---|---|---|
deleted |
true |
(required) |
id |
uuid |
(required) |
Link
| Field | Type | |
|---|---|---|
id |
uuid |
(required) |
label |
string | null |
(required) |
url |
string | null |
(required) |
icon |
string | null |
(required) |
position |
integer |
(required) |
visible |
boolean |
(required) |
created_at |
date-time |
(required) |
updated_at |
date-time |
(required) |
LinkCreate
| Field | Type | |
|---|---|---|
label |
string | null |
120 characters by default; your organisation's number is link_label_chars in GET /usage. (max 120) |
url |
string |
(required; min 1; max 2048) |
icon |
string | null |
(max 32) |
position |
integer |
(>= 0; <= 10000) |
visible |
boolean |
LinkList
| Field | Type | |
|---|---|---|
data |
Link[] |
At most 1,000 for a page by default: a safety ceiling the same on every plan. A page that had more when its ceiling was lowered keeps them, and every one is listed, up to 5,000, the most any page can ever hold. (required; <= 5000 items) |
LinkUpdate
| Field | Type | |
|---|---|---|
label |
string | null |
120 characters by default; your organisation's number is link_label_chars in GET /usage. (max 120) |
url |
string |
(min 1; max 2048) |
icon |
string | null |
(max 32) |
position |
integer |
(>= 0; <= 10000) |
visible |
boolean |