Skip to content
Browse the docs

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.

On this page

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_LIMIT in Configuration). The ones that don’t fit come back with status: "pending" and deferred: 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 answers 200 with stopped: 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, never publishing), so send those ids again. A 500 means nothing was recorded; check the draft before retrying.
  • Do not call /api/targets/:id/retry unless the row is failed (or a stuck publishing older than 15 minutes).
  • Schedule returns 409 if that account is already published or still publishing. Check error, alreadyPublished, and inFlight instead 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_TOKEN still does.

Written for CogSend 1.3.2. This page lives in the app repo, next to the code it describes. Something wrong or missing? Edit docs/api.md or open an issue.