REST API v1
At /api/v1, authenticated with an API key.
Scripts and AI agents act in one workspace with the scopes their key was given. Every post they create gets source “api”.
At /api/v1, authenticated with an API key.
At /mcp (Streamable HTTP), with an API key or an OAuth token from claude.ai or ChatGPT.
calmposts-mcp runs the same tools over stdio, on top of the REST API.
The machine-readable reference is https://calmposts.com/api/v1/openapi.json (OpenAPI 3.1, public).
The workspace's owner creates keys in API & MCP in the app and picks a name and scopes. The key (cp_live_…) is shown once; calmposts keeps only its SHA-256 hash and its first 12 characters.
A revoked key is refused on its next request. A key also stops working when its creator is blocked, deleted or leaves the workspace. Your plan sets how many keys you can hold; the free plan has none.
Every request sends Authorization: Bearer cp_live_…. Each key gets 60 requests a minute, REST and MCP together; past that the answer is 429 rate_limited with Retry-After in seconds.
| Scope | Allows |
|---|---|
accounts:read | GET /accounts, GET /limits |
posts:read | GET /posts, GET /posts/:id, GET /queue/next |
posts:write | POST /posts, PATCH /posts/:id, DELETE /posts/:id, POST /posts/:id/retry |
media:write | POST /media |
Errors share one envelope: {"error": "…", "code": "…", "field": "…"}.
A post that fails validation is a 422: field and code name the first problem (targets[0].parts[0].text, too_long) and the errors array lists them all. A quota is 402 quota_exceeded with the metric as field; a missing scope is 403 insufficient_scope.
Keep the key in an environment variable; the examples below reuse $auth.
export CALMPOSTS_API_KEY=cp_live_…
export CALMPOSTS=https://calmposts.com
auth=(-H "Authorization: Bearer $CALMPOSTS_API_KEY")Each connected account with its network's rules and its character limit.
curl "${auth[@]}" $CALMPOSTS/api/v1/accounts
# {"items":[{"id":"…","provider":"x","handle":"@carol","status":"active","char_limit":280,"capabilities":{…}}]}The plan's usage this period, the workspace's time zone and each network's rules.
curl "${auth[@]}" $CALMPOSTS/api/v1/limits
# {"timezone":"Europe/Berlin","billing":{"plan":{…},"usage":{"posts_per_day":{"used":3,"limit":30,…}}},"networks":[…]}schedule.mode is draft, queue (the accounts' next free slot), at (with an RFC 3339 time) or now. Several parts make a thread; a target's own parts give that channel its own wording.
curl -X POST $CALMPOSTS/api/v1/posts "${auth[@]}" -H "Content-Type: application/json" -d '{
"parts": [{"text": "Calm posts, scheduled."}],
"targets": [{"account_id": "ACCOUNT_ID"}],
"schedule": {"mode": "queue"}
}'
# 201 {"id":"…","status":"scheduled","scheduled_at":"…","version":1,"source":"api",…}Upload first, then pass the media id in a part.
curl -X POST $CALMPOSTS/api/v1/media "${auth[@]}" -F file=@cat.png -F alt="A calm cat"
# 201 {"id":"MEDIA_ID","kind":"image",…} → "parts": [{"text": "…", "media_ids": ["MEDIA_ID"]}]Filter by status, account_id, from and to (RFC 3339), q, limit and offset.
curl "${auth[@]}" "$CALMPOSTS/api/v1/posts?status=scheduled,failed&limit=20" # {"items":[…],"total":n}
curl "${auth[@]}" $CALMPOSTS/api/v1/posts/POST_IDPATCH changes what the body names, from the version you read; a stale version is 409 version_conflict. Only drafts and scheduled posts can be edited.
curl -X PATCH $CALMPOSTS/api/v1/posts/POST_ID "${auth[@]}" -H "Content-Type: application/json" \
-d '{"version": 1, "schedule": {"mode": "at", "at": "2026-10-21T09:30:00+02:00"}}'DELETE cancels a scheduled post (a published one stays on the network). retry sends the failed channels again now. queue/next is where “queue” would put a post.
curl -X DELETE "${auth[@]}" $CALMPOSTS/api/v1/posts/POST_ID # 204
curl -X POST "${auth[@]}" $CALMPOSTS/api/v1/posts/POST_ID/retry # the post
curl "${auth[@]}" "$CALMPOSTS/api/v1/queue/next?account_ids=A,B" # {"at":"2026-10-21T09:00:00Z"}Instead of polling, let calmposts tell your system what happened. Each delivery is a POST with a JSON body; the id is unique, so a receiver can drop a repeat.
| Event | Sent when |
|---|---|
post.published | every channel of a post went out (data: post_id and each target's url) |
post.failed | a channel of a post failed for good (data: post_id, target_id, error_code, error) |
post.pending_approval | an agent's post waits for a person to approve it |
account.needs_reconnect | a network revoked the access and the account needs reconnecting |
{
"id": "…",
"type": "post.published",
"workspace_id": "…",
"created_at": "2026-10-21T09:00:03Z",
"data": {"post_id": "…", "targets": [{"id": "…", "account_id": "…", "provider": "x", "url": "https://x.com/…"}]}
}The body carries no token or secret.