DocsPublish and automate
API keys
Use a personal key from scripts, Shortcuts and cron jobs. The full endpoint reference lives in your own instance, at /api.
The full reference lives in your instance. Every endpoint, its payload and what it answers is listed inside your own deployment at
/api, next to the examples and the copy buttons. This page is what has to be read first: making a key, and making the first call.
The browser UI uses the cog_session cookie after TOTP. Scripts, Shortcuts and
cron use a personal API key instead — no login, no cookies. Manage it in
Settings → API access (generate, rotate, revoke); the raw key is shown once
and only its hash is stored. Worked examples for the common calls live in-app at
/api.
Authentication
Export your instance URL and a key from Settings → API access:
export APP_URL=https://cogsend.<account>.workers.dev
export COGSEND_API_KEY=cog_...
curl -s "$APP_URL/api/connections" -H "Authorization: Bearer $COGSEND_API_KEY"
X-API-Key works as an alternative header; never put the key in the URL. The key
acts as you on drafts, variants, media, publish, schedule, queue, settings, and
reads — but it can never connect, re-verify or disconnect accounts, or create,
rotate, or revoke keys (those stay in the browser session). The global
API_TOKEN Worker secret still works as a bearer for backwards compatibility, on
exactly the same routes as a personal key — it cannot reach the session-only ones
either — but prefer the personal key for scripts: it is revocable without
touching the scheduler.
Examples
Every call below needs a connection id, and the draft call hands back a draft id.
GET /api/connections lists the connected accounts; id is the value the
publish and schedule calls take. With jq installed:
curl -s "$APP_URL/api/connections" -H "Authorization: Bearer $COGSEND_API_KEY" \
| jq -r '.connections[] | "\(.id) \(.platform) \(.handle)"'
Create a draft, then publish it to one account:
curl -s -X POST "$APP_URL/api/drafts" \
-H "Authorization: Bearer $COGSEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title":"Hello","baseBody":"from a script"}'
curl -s -X POST "$APP_URL/api/drafts/DRAFT_ID/publish" \
-H "Authorization: Bearer $COGSEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{"connectionIds":["CONN_ID"]}'
Limits
- A body is capped at 100,000 characters, and a variant may carry at most 100 explicit
threadSegments. - Each platform’s own text and media limits are checked again at publish, so what the API accepts is not necessarily what a platform will take.
Publishing behaviour
- Publishing the same draft and account twice reuses the row. Already-published accounts come back
skipped: true. - A publish that is still running on that account answers 409 with
inFlight(the connection ids) — wait, then try again. - A retried segment carries the same platform-side id as its first attempt, so a thread that failed half-way does not double-post what already went out (Mastodon remembers the id for an hour, Bluesky refuses to overwrite the record).
- Sending several connection ids in one request publishes them in order. The first always runs; each further one runs only if it fits in what is left of the request’s Cloudflare call budget (50 on Workers Free, see
SUBREQUEST_LIMITin Configuration). The ones that don’t fit come back withstatus: "pending"anddeferred: true. They are already due and go out on the next scheduler tick, so don’t send them again. For the fastest results, send one connection id per request. If a request still runs out, it answers200withstopped: true,stoppedError, and the results it did get — the accounts after the last entry were not completed and are still due (a target the failure interrupted is left retryable, neverpublishing), so send those ids again. A500means nothing was recorded; check the draft before retrying. - Do not call
/api/targets/:id/retryunless the row isfailed(or a stuckpublishingolder than 15 minutes). - Schedule returns 409 if that account is already published or still publishing. Check
error,alreadyPublished, andinFlightinstead of treating HTTP 200 as “it was scheduled”.
More examples
Schedule a draft instead of publishing it. runAt is an ISO 8601 time, in the
future and no more than a year out, and one request takes at most ten connection
ids:
curl -s -X POST "$APP_URL/api/drafts/DRAFT_ID/schedule" \
-H "Authorization: Bearer $COGSEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{"connectionIds":["CONN_ID"],"runAt":"2030-01-01T09:00:00Z"}'
It answers with the scheduled targets and scheduledFor. The post then waits
for a tick, like one scheduled from the composer.
See what is queued:
curl -s "$APP_URL/api/queue" -H "Authorization: Bearer $COGSEND_API_KEY"
Where to go next
- Scheduling — the tick that publishes a scheduled post, and the bearer it needs.
- Cloudflare Access — if the instance is behind Access, a script needs a service token as well as a key.
- Configuration — where keys and secrets are stored, and what
API_TOKENstill does.