A post is written once and goes out to one or more accounts. Each account it goes to is a target with its own status, link and error — a post can publish on Instagram and fail on X.
{
"id": "5e0c2b7a-1d94-4f38-a6e2-7b3c9d10f4a8",
"body": "Autumn catalogue is out. Link in bio.",
"media": [
{
"id": "e71b0f3d-2c85-4a9e-b604-93d2f7a1c5e8",
"mime_type": "image/jpeg",
"bytes": 482113,
"url": "https://.../social-media/e71b0f3d-.../catalogue.jpg",
"status": "ready",
"created_at": "2026-09-30T10:05:40.551020+00:00"
}
],
"options": { "instagram": { "type": "feed" } },
"status": "scheduled",
"scheduled_for": "2026-10-01T09:00:00+00:00",
"published_at": null,
"source": "api",
"created_at": "2026-09-30T10:06:02.118904+00:00",
"updated_at": "2026-09-30T10:06:02.118904+00:00",
"targets": [
{
"id": "b8f24c61-0e3d-4a97-8c15-2d6e9f7a3b40",
"account_id": "a3d9f1c4-7b2e-4c60-9e85-1f4b6d2a0c73",
"network": "instagram",
"handle": "acme",
"status": "pending",
"external_url": null,
"last_error": null,
"published_at": null,
"metrics": null
}
]
}
A post's status is draft, scheduled, publishing, published, partial
(some targets published, some failed), failed or cancelled. A target's is
pending, publishing, processing (the network accepted it and is still
working — a video transcoding), published, failed or cancelled.
external_url is the live post once it exists. source is dashboard, api
or mcp.
options and metrics are passed through in each network's own vocabulary,
so their keys are not renamed to snake_case the way every other key is.
Operations
GET /v1/posts
List posts
Scope social:read.
The workspace's posts, newest first, optionally filtered.
Parameters
| Name | In | Type | |
|---|---|---|---|
status |
query | PostStatus |
(one of draft, scheduled, publishing, published, partial, failed, cancelled) |
from |
query | date-time |
An ISO-8601 timestamp with an offset. |
to |
query | date-time |
An ISO-8601 timestamp with an offset. |
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 | PostList |
A page of posts. |
| 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. |
Scheduled and published posts — answers 200
Ordered by when the post is scheduled, newest first. status filters to one state.
curl "https://api.boltclaw.ai/v1/posts?limit=50" \
-H "Authorization: Bearer bc_live_EXAMPLExxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
const res = await fetch("https://api.boltclaw.ai/v1/posts?limit=50", {
headers: {
Authorization: `Bearer ${process.env.BOLTCLAW_KEY}`,
},
});
// 200
const data = await res.json();
POST /v1/posts
Create a post
Scope social:write. Accepts Idempotency-Key. Can make something publicly visible. Can record a platform charge.
One post to one or more connected accounts. Say when with exactly one of scheduled_for,
publish_now: true or draft: true. A post needs a body or at least one media id. The
organisation's monthly post allowance applies (402 PLAN_LIMIT). Posting to X is billed
per call on top of the plan.
Request body (required)
| Field | Type | |
|---|---|---|
body |
string |
(max 5000) |
media_ids |
uuid[] |
(<= 20 items) |
account_ids |
uuid[] |
(required; <= 50 items; >= 1 items) |
scheduled_for |
date-time |
|
publish_now |
boolean |
|
draft |
boolean |
|
options |
PostOptions |
Per-network options, keyed by network, in each network's own vocabulary. |
{
"account_ids": "3f8c1e42-9b7a-4d61-a0f5-2c9e1b7d4a83"
}
Answers
| Status | Body | |
|---|---|---|
| 201 | Post |
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. |
| 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. |
| 422 | ErrorEnvelope |
VALIDATION_ERROR: the request is well formed but cannot be done as asked, such as a network that does not support it. Or UPSTREAM_ERROR: the network refused it. |
| 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. |
| 502 | ErrorEnvelope |
UPSTREAM_ERROR: the social network failed the call. The message says what happened. |
| 503 | ErrorEnvelope |
UPSTREAM_ERROR: the social network is temporarily unavailable; retrying later may work. |
GET /v1/posts/{id}
Read a post
Scope social:read. Another workspace's answers 404.
The post, its media and where it stands on each account.
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 | Post |
The post. |
| 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/posts/{id}
Update a post
Scope social:write. Another workspace's answers 404. Can make something publicly visible. Can record a platform charge.
Changes a draft or scheduled post. Any other is 409 CONFLICT.
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 | |
|---|---|---|
body |
string |
(max 5000) |
media_ids |
uuid[] |
(<= 20 items) |
account_ids |
uuid[] |
(<= 50 items; >= 1 items) |
scheduled_for |
date-time | null |
|
options |
PostOptions |
Per-network options, keyed by network, in each network's own vocabulary. |
Answers
| Status | Body | |
|---|---|---|
| 200 | Post |
The updated post. |
| 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. |
| 422 | ErrorEnvelope |
VALIDATION_ERROR: the request is well formed but cannot be done as asked, such as a network that does not support it. Or UPSTREAM_ERROR: the network refused it. |
| 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/posts/{id}
Delete a post
Scope social:write. Another workspace's answers 404.
Deletes the post from BoltClaw.
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 |
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. |
| 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. |
| 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. |
POST /v1/posts/{id}/cancel
Cancel a scheduled post
Scope social:write. Accepts Idempotency-Key. Another workspace's answers 404.
Stops a post that has not published yet. Takes no body.
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 | Post |
The cancelled post. |
| 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. |
| 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) |
Media
| Field | Type | |
|---|---|---|
id |
uuid |
(required) |
mime_type |
string |
(required) |
bytes |
integer | null |
(required) |
url |
string | null |
(required) |
status |
string |
(required; one of pending, ready) |
created_at |
date-time |
(required) |
Network
One of: instagram, facebook, threads, bluesky, x, tiktok, linkedin, youtube, pinterest.
NextCursor
Pass back as cursor for the next page; null on the last page.
string | null.
Post
| Field | Type | |
|---|---|---|
id |
uuid |
(required) |
body |
string |
(required) |
media |
Media[] |
(required) |
options |
PostOptions |
Per-network options, keyed by network, in each network's own vocabulary. (required) |
status |
PostStatus |
(required; one of draft, scheduled, publishing, published, partial, failed, cancelled) |
scheduled_for |
date-time | null |
(required) |
published_at |
date-time | null |
(required) |
source |
string |
(required; one of dashboard, api, mcp) |
created_at |
date-time |
(required) |
updated_at |
date-time |
(required) |
targets |
Target[] |
(required) |
PostCreate
Exactly one of scheduled_for, publish_now: true or draft: true, and a body or at
least one media id.
| Field | Type | |
|---|---|---|
body |
string |
(max 5000) |
media_ids |
uuid[] |
(<= 20 items) |
account_ids |
uuid[] |
(required; <= 50 items; >= 1 items) |
scheduled_for |
date-time |
|
publish_now |
boolean |
|
draft |
boolean |
|
options |
PostOptions |
Per-network options, keyed by network, in each network's own vocabulary. |
PostList
| Field | Type | |
|---|---|---|
data |
Post[] |
(required) |
next_cursor |
NextCursor |
Pass back as cursor for the next page; null on the last page. (required) |
PostOptions
Per-network options, keyed by network, in each network's own vocabulary.
object.
PostStatus
One of: draft, scheduled, publishing, published, partial, failed, cancelled.
PostUpdate
| Field | Type | |
|---|---|---|
body |
string |
(max 5000) |
media_ids |
uuid[] |
(<= 20 items) |
account_ids |
uuid[] |
(<= 50 items; >= 1 items) |
scheduled_for |
date-time | null |
|
options |
PostOptions |
Per-network options, keyed by network, in each network's own vocabulary. |
Target
| Field | Type | |
|---|---|---|
id |
uuid |
(required) |
account_id |
uuid |
(required) |
network |
Network |
(required; one of instagram, facebook, threads, bluesky, x, tiktok, linkedin, youtube, pinterest) |
handle |
string | null |
(required) |
status |
string |
(required; one of pending, publishing, processing, published, failed, cancelled) |
external_url |
string | null |
(required) |
last_error |
string | null |
(required) |
published_at |
date-time | null |
(required) |
metrics |
object | null |
The network's own counters, by the network's own names. (required) |