The assistant route is a sentence and a yes. Sometimes you do not want a conversation; you want a node in a flow, a cron job, or a line in a deploy script that posts the release note when the build goes green. Everything the assistant can do through the MCP server exists as plain HTTP at /api/v1, with the same validators, the same limits and the same receipts. This post is the wiring. The reference, with every field, is at /docs/api.
Auth and limits
Every request carries your account API key as a Bearer token. The key is on the Account page. Treat it as a password: regenerating it signs out every client holding the old one, including assistants connected over MCP.
curl https://posteora.com/api/v1/accounts \
-H "Authorization: Bearer $POSTEORA_API_KEY"
The API accepts 120 requests a minute per key and answers 429 above that. GET /api/v1/limits returns your plan limits: the schedule horizon (14 days on Free, 90 on Pro), the queue cap (10 and 300), the daily caps per network on Pro, the 48-hour duplicate window and your X allowance meter.
GET /api/v1/accounts is worth calling first in any flow. Each account carries health from the daily token check; needs_reconnect means a post to that network will fail until the owner reconnects on the website, so a flow can stop early.
Publish now
Publishing needs an Idempotency-Key header. Any unique string up to 128 characters; a UUID is fine.
curl -X POST https://posteora.com/api/v1/posts \
-H "Authorization: Bearer $POSTEORA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7" \
-d '{"text": "Shipped the webhook docs today.", "targets": ["x", "bluesky"]}'
The answer comes after every network has answered: results with one entry per network (the live url, a status_label and a record_id), the X allowance meter, a one-line receipt ("Published on X and Bluesky. X allowance: 19 posts with links or 259 plain posts left.") and replayed: false.
A network that refuses the post gets its own entry with "ok": false and a readable message; the others still go out, and the HTTP status stays 201 because the request itself was valid. Check results, not just the status code.
A retry with the same key and the same account gets the stored answer back with Idempotency-Replayed: true and never posts twice. A retry that arrives while the first request is still running answers 409 with code in_flight; wait a few seconds and send it again. Without the header, publishing answers 400 with idempotency_key_required.
Schedule
Add scheduled_for as ISO 8601 with an offset and the post is queued instead of sent:
curl -X POST https://posteora.com/api/v1/posts \
-H "Authorization: Bearer $POSTEORA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"text": "Tuesday thread, part one.", "targets": ["x"],
"scheduled_for": "2026-10-20T09:00:00-07:00", "media_ids": [42]}'
You get a post_id, the queued status, and a receipt that prints the time in the offset you sent. Other fields: thread (2 to 25 posts sent as one chained item, scheduling only), options per network (required for TikTok, optional privacy_status for YouTube), reply_to (a URL of one of your own X posts; X must be the only target), and draft: true to save into the Studio without queuing.
Media goes up first with POST /api/v1/media (multipart, raw bytes, or JSON with a public https URL); pass the returned media_id in media_ids.
Reading back
GET /api/v1/posts?status=pending,draft is the queue, GET /api/v1/posts/{post_id} one post with its per-network rows, DELETE /api/v1/posts/{post_id} a cancel (a sent post answers 400), and GET /api/v1/activity?status=posted,failed one row per network per send, newest first: the polling source when you cannot receive webhooks.
Webhooks
Register up to three HTTPS endpoints on the Account page or by API. Each gets its own secret.
curl -X POST https://posteora.com/api/v1/webhooks \
-H "Authorization: Bearer $POSTEORA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/hooks/posteora", "events": ["post.sent", "post.failed"]}'
Omit events to receive everything. The six events:
| Event | When |
|---|---|
post.scheduled |
A post entered the queue (scheduled, or a draft approved) |
post.sent |
One network published it. One event per network, so a post to three networks sends three |
post.failed |
One network refused it, or the scheduler refused it at send time (daily cap, missing TikTok choices) |
post.cancelled |
A queued post was cancelled |
account.needs_reconnect |
The daily token check found a connection the network no longer accepts |
webhook.test |
The "Send test event" button, or POST /api/v1/webhooks/{id}/test |
Events are queued the moment they happen and sent by the scheduler within about a minute, after the posts of that tick are settled, so a slow endpoint never delays a send.
Delivery. A POST with Content-Type: application/json and three headers: X-Posteora-Signature, X-Posteora-Event (the event name) and X-Posteora-Delivery (a unique id you can use to drop duplicates). Answer any 2xx within 5 seconds. Anything else is retried: 4 attempts in all, waiting 30 seconds, then 120, then 600 between them, never more than one attempt a second per account. The last status and error of each endpoint show next to it on the Account page and in GET /api/v1/webhooks.
Verify the signature. The header is t=<unix time>,v1=<hex>; v1 is the HMAC-SHA256 of "<t>.<raw body>" with the webhook's secret. Compare in constant time, reject a t older than 300 seconds, and hash the raw request body, not a re-serialised one. The docs page has the function in Python and in Node, copied from the code.
n8n
Two nodes cover everything.
Posting: an HTTP Request node. Method POST, URL https://posteora.com/api/v1/posts, authentication "Header Auth" with name Authorization and value Bearer <your key>, JSON body as above. For publish-now add a second header Idempotency-Key set to an expression such as {{ $execution.id }} so a re-run of the workflow cannot double-post. For scheduling, put the ISO time with its offset in scheduled_for and skip the key.
Listening: a Webhook node. Copy its production URL into POST /api/v1/webhooks (or the Account page), choose the events, and add a Code node that checks X-Posteora-Signature before anything else. Respond 204 at once; do the work after. A post.failed event with error_code auth_expired wants an email to the owner with the Connections link, not a retry.
The API docs describe a community node package and a Zapier app for Posteora; until you see them in your directory, the generic HTTP nodes above do the same job with the same requests.
Zapier
Use Webhooks by Zapier at both ends.
- Action, Custom Request:
POSTtohttps://posteora.com/api/v1/postswith theAuthorizationandContent-Typeheaders, andIdempotency-Keyset to the Zap run id when publishing now. - Trigger, Catch Hook: register the hook URL with
events: ["post.sent", "post.failed"]. Catch Hook parses the body for you, which makes the signature check awkward, so treat it as a notification and read the real state in a second step:GET /api/v1/posts/{post_id}.
Your own scripts
A deploy script that announces a release is one request: POST /api/v1/posts with the Bearer header, a fresh UUID as Idempotency-Key, and {"text": "v2.4 is live. Notes in the reply.", "targets": ["x", "bluesky", "mastodon"]}. Print receipt, then exit non-zero if any entry in results has "ok": false, so a failed network fails the pipeline step instead of hiding in a log. Make the key a CI secret, keep the link out of the X text (X ranks link posts lower and they draw more of the allowance), and send the link as a second request with reply_to set to the X URL from results.
Errors you will meet
Every error is JSON: {"ok": false, "error": "readable message", "code": "optional_code"}.
| Status | Meaning |
|---|---|
400 |
Invalid field, or a network's own rule: text too long, media required, connect the network first |
409 |
in_flight: the same Idempotency-Key is still being processed |
422 |
A plan limit: horizon, queue_cap, daily_cap, monthly_cap, duplicate |
429 |
Over 120 requests a minute for this key |
A 422 is not a bug in your flow. It is the same refusal the composer and the assistant would show in the preview receipt, arriving one step later because a script has no preview step. If you want one, call the same endpoint with draft: true first and inspect the post, or keep the conversation route for the posts that deserve a human yes: Claude Code in two minutes.
The free plan includes the API and webhooks, with 10 posts a month and no card. Pro is $19 a month with unlimited posts under fair use; the pricing page has the table, and every change to the API lands dated on the changelog.