Skip to content

Postacio for developers

A REST API and an MCP server, both working on the same groups, drafts and schedules you see in the app. Scripts use an API key; AI agents like Claude and ChatGPT sign in with your account instead.

The reference.

From your first key to a verified webhook.

Get an API key

  1. 1
    In Postacio, open Settings → API & AI agents and choose Create key.
  2. 2
    Name it after what will use it - "Newsletter bot" - because every draft and change it makes is signed with that name.
  3. 3
    Choose the groups it may use (all of them by default) and whether it may schedule and publish. Publishing is off unless you turn it on: the key then only creates and edits drafts, and you schedule or publish them in Postacio.
  4. 4
    Copy the key. It starts with pk_live_ and is shown once; Postacio keeps only a hash of it.

Send it as a bearer token on every request:

Shell
curl https://postacio.com/api/v1/me \
  -H "Authorization: Bearer $POSTACIO_API_KEY"

You can have up to 20 active keys. Revoking one in Settings takes effect on its next request, which then answers 401 revoked.

Connect an AI agent

Agents connect over MCP at https://postacio.com/mcp. They sign in with OAuth: Postacio asks you which groups the agent may use and whether it may publish (off unless you turn it on, so a new agent writes drafts only), and you can change or revoke that later in Settings → API & agents.

Claude

  1. 1
    Open Claude and go to Settings → Connectors.
  2. 2
    Choose Add custom connector.
  3. 3
    Paste https://postacio.com/mcp and choose Connect.
  4. 4
    Approve the connection in the browser window that opens.

What to ask Claude

ChatGPT

  1. 1
    Open ChatGPT and go to Settings → Security and login, then turn on Developer mode.
  2. 2
    Go to Settings → Connectors and choose Create.
  3. 3
    Paste https://postacio.com/mcp and choose OAuth for authentication.
  4. 4
    Approve the connection in the browser window that opens.

Developer mode needs a paid ChatGPT plan.

What to ask ChatGPT

Claude Code

  1. 1
    Run this command in your terminal.
    Terminal
    claude mcp add --transport http postacio https://postacio.com/mcp
  2. 2
    In Claude Code, run /mcp and sign in to Postacio.

What to ask Claude Code

Any other MCP client

  1. 1
    Paste https://postacio.com/mcp where your client adds an MCP server.
  2. 2
    It signs in with OAuth. No API key needed.

The MCP tools mirror the API one to one: listing groups and accounts, creating, editing, planning, scheduling and publishing drafts of every kind, approvals, review comments, media from a file or a link, tags, posting slots, posting goals and analytics. Only webhooks have no tool, because an agent has no address to receive them. An agent is told to confirm with you before it schedules, publishes or deletes, to write a reply or quote only when you ask, and never to post the same text to two accounts of one network.

The server also offers three ready-made prompts your client can show as actions: plan_week, review_scheduled and repurpose_top_post.

Your first draft

A worked example with a key that may publish. Set POSTACIO_API_KEY first.

1. Find your group and an account

Shell
curl https://postacio.com/api/v1/groups \
  -H "Authorization: Bearer $POSTACIO_API_KEY"

curl https://postacio.com/api/v1/groups/$GROUP_ID/accounts \
  -H "Authorization: Bearer $POSTACIO_API_KEY"

2. Create a draft in the next free slot

next-free-slot picks the group's next open posting slot for those accounts. Idempotency-Key makes a retried request safe: the same key returns the same draft instead of a second one.

Shell
curl -X POST https://postacio.com/api/v1/groups/$GROUP_ID/drafts \
  -H "Authorization: Bearer $POSTACIO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: first-draft-001" \
  -d '{
    "text": "Hello from the Postacio API",
    "targets": [{ "account_id": "ACCOUNT_ID" }],
    "publish_at": "next-free-slot"
  }'

3. Read it back

Shell
curl https://postacio.com/api/v1/groups/$GROUP_ID/drafts/$DRAFT_ID \
  -H "Authorization: Bearer $POSTACIO_API_KEY"
Response
{
  "id": "…",
  "status": "scheduled",
  "publish_state": "idle",
  "scheduled_at": "2026-09-14T09:00:00.000Z",
  "origin": { "kind": "api", "name": "Newsletter bot" },
  "targets": [{ "account_id": "…", "platform": "x", "status": "pending", "permalink": null, "error": null }]
}

The draft is on the calendar in the app, marked API, and its activity log says "Newsletter bot (API)".

Scheduling choices

You sendThe draft becomesNeeds publishing
neither dateA draftNo
"plan_at": "<datetime>"Planned for that date. It won't publish on its own.No
"plan_at": "next-free-slot"Planned into the next open slotNo
"publish_at": "<datetime>"Scheduled to publish thenYes
"publish_at": "next-free-slot"Scheduled into the next open slotYes
"publish_at": "now"Publishing immediately (202)Yes
  • Datetimes are ISO 8601 with an offset, for example 2026-09-14T09:00:00+02:00.
  • next-free-slot looks 60 days ahead. With no open slot it answers 422 validation with param: "publish_at".
  • jitter_minutes (0 to 15) moves the publish time by a small random amount so posts don't all land on the minute.
  • Publishing is asynchronous. After publish or "publish_at": "now", read the draft until publish_state is finished; each target then carries its permalink or error.
  • An existing draft can be moved with POST …/plan, POST …/schedule and POST …/publish, and taken back with DELETE …/plan or DELETE …/schedule.

Kinds and options

kind says what a draft is. It defaults to post and can't change after the draft is created.

KindNetworksYou also send
postAll-
reelYouTubeA video in media
pollX, LinkedIn, Telegrampoll. The text is the question.
articleOne X account with X Premium-
replyOne X accounttarget_url, a link to the X post
quoteOne X accounttarget_url, a link to the X post
repostOne LinkedIn accounttarget_url, a link to the LinkedIn post

A reel

Upload the video with POST …/media, PUT the bytes, then verify it. Put the answer from verify in media as it is.

Shell
curl -X POST https://postacio.com/api/v1/groups/$GROUP_ID/drafts \
  -H "Authorization: Bearer $POSTACIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "reel",
    "text": "Behind the scenes",
    "media": [{ "key": "…", "kind": "video", "content_type": "video/mp4", "name": "bts.mp4" }],
    "targets": [{ "account_id": "YOUTUBE_ACCOUNT_ID", "options": { "title": "Behind the scenes", "madeForKids": "no" } }]
  }'

A poll

Shell
curl -X POST https://postacio.com/api/v1/groups/$GROUP_ID/drafts \
  -H "Authorization: Bearer $POSTACIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "poll",
    "text": "Which topic next week?",
    "poll": { "options": ["Pricing", "Hiring", "Tooling"], "duration_days": 1 },
    "targets": [{ "account_id": "X_ACCOUNT_ID" }]
  }'

X and LinkedIn take 2 to 4 answers; Telegram takes up to 12. X polls run 1, 3 or 7 days, LinkedIn polls also 14. A Telegram poll stays open unless it sets close_after_days, and only a Telegram poll can be a quiz or take multiple_answers.

Per-platform options

targets[].options holds the same settings the composer shows for that account's network, such as X reply settings, LinkedIn visibility or the Telegram link preview. The OpenAPI document lists every option with its type, allowed values and default, one shape per network:

Shell
curl -s https://postacio.com/api/v1/openapi.json \
  | jq '.components.schemas.CreateDraft.properties.targets.items.properties.options.anyOf[]
        | { network: .title, options: (.properties | keys) }'

An unknown option or a value outside its choices is a 400 validation whose param names it, for example targets[0].options.replySettings.

Review comments

Read and write the same review comments as the draft's comment panel. A comment from a key or agent is signed with its name, and @Name notifies that teammate or reviewer. Comments are flat, so to reply you add another comment.

Shell
curl "https://postacio.com/api/v1/groups/$GROUP_ID/drafts/$DRAFT_ID/comments?resolved=false" \
  -H "Authorization: Bearer $POSTACIO_API_KEY"

curl -X POST https://postacio.com/api/v1/groups/$GROUP_ID/drafts/$DRAFT_ID/comments \
  -H "Authorization: Bearer $POSTACIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "body": "Shorter hook? @Ana", "thread_post_index": 0 }'

curl -X PATCH https://postacio.com/api/v1/groups/$GROUP_ID/drafts/$DRAFT_ID/comments/$COMMENT_ID \
  -H "Authorization: Bearer $POSTACIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "resolved": true }'

thread_post_index pins a comment to one post of a thread (0 is the first). A key may resolve, reopen ("resolved": false) or delete any comment on a draft it can reach, whoever wrote it.

Posting slots

Slots are the group's recurring posting times, the ones next-free-slot fills. Change them one at a time:

Shell
curl -X POST https://postacio.com/api/v1/groups/$GROUP_ID/queue/slots \
  -H "Authorization: Bearer $POSTACIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "label": "Morning", "days": [0, 2, 4], "minutes": 540, "account_ids": ["ACCOUNT_ID"] }'

curl -X PATCH https://postacio.com/api/v1/groups/$GROUP_ID/queue/slots/$SLOT_ID \
  -H "Authorization: Bearer $POSTACIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "minutes": 600 }'
  • days run 0 (Monday) to 6 (Sunday). minutes is the time after midnight in the group's timezone, so 540 is 09:00.
  • PATCH changes only the fields you send. starts_on, ends_on and skip_dates limit when a slot runs.
  • Changing or deleting a slot doesn't move posts that are already scheduled.

Media from a link

Have a public link rather than the file? Postacio downloads it and stores it like an upload. The answer goes straight into a draft's media list:

Shell
curl -X POST https://postacio.com/api/v1/groups/$GROUP_ID/media/import \
  -H "Authorization: Bearer $POSTACIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/chart.png", "alt_text": "Signups by month" }'

Only public https links are fetched, up to 50 MiB, following at most three redirects. A link that needs a sign-in, or points at a private address, is a 400.

Approvals

In a group that requires approval, a post can't be scheduled or published until it is approved at its current version. Every draft reports where it stands in approval: required, status and current.

Shell
curl -X POST https://postacio.com/api/v1/groups/$GROUP_ID/drafts/$DRAFT_ID/approval \
  -H "Authorization: Bearer $POSTACIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "action": "approve", "expected_version": 7 }'
  • request asks the group's approvers and needs write access. approve and request_changes need publishing permission; changes need a comment.
  • An approval names the version you reviewed. If the draft changed since, the answer is 409: read it again.
  • A key or agent can't approve a draft a key or agent wrote (403 forbidden): a person approves it in Postacio.
  • A reply to, or quote of, someone else's X post that a key or agent wrote doesn't publish until a person approves it in Postacio. At its time it is held with hold_reason needs_person, and the draft says so beforehand in warnings.
  • Filter the list with ?approval=pending, and by account_id or a from/to window with sort=time.

Group analytics

GET /groups/$GROUP_ID/analytics/summary answers a group's numbers in one call: posts published, each metric summed, and the change in followers, per account and in total.

Errors

Every error has the same shape:

Response
{
  "error": {
    "code": "validation",
    "message": "…",
    "param": "targets[ACCOUNT_ID].text",
    "detail": [{ "code": "…", "message": "…", "account_id": "…" }]
  }
}
StatusCodeMeaning
400, 422validationThe request is malformed or a target can't take it. param and detail say where.
401unauthorizedNo key or token, or one Postacio doesn't recognise.
401revokedThe key or the agent connection was revoked in Settings.
402account_pausedThe account's subscription is paused.
402plan_limitThe request is over the account's plan limits.
403publish_not_allowedThis key or connection may not schedule or publish.
404not_foundNot there, or not in a group this key may use.
409conflictThe draft is publishing or already finished and can't change.
409idempotency_conflictAn Idempotency-Key was reused with a different request.
429rate_limitedToo many requests. Wait for Retry-After.
500internalSomething failed on our side. Retrying with the same Idempotency-Key is safe.
503maintenancePostacio is closed for maintenance. Wait for Retry-After.

Rate limits

120 / 30
reads / writes a minute, per key or agent
300
requests a minute, per account across all keys and agents
1 : 1
an MCP tool call counts like the API request it maps to

Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (unix seconds) for the key, and the same three with -User- in the name for the account. A 429 adds Retry-After.

Webhooks

Add an address in Settings → API & AI agents → Webhooks and choose the events you want. Postacio sends a JSON POST for each one, whoever made the change - you in the app, a key or an agent.

EventWhen
draft.createdA draft was created.
draft.updatedA draft's content or settings changed.
draft.plannedA draft was given a date. It won't publish on its own.
draft.scheduledA draft was scheduled to publish.
draft.unscheduledA scheduled draft was taken off the schedule.
draft.publishedEvery destination published.
draft.partially_failedSome destinations published and some failed.
draft.failedEvery destination failed.
draft.deletedA draft was deleted. The payload carries only its id and group.

What arrives

HTTP request
POST /your/endpoint
Content-Type: application/json
User-Agent: Postacio-Webhooks/1
Postacio-Event: draft.published
Postacio-Delivery-Id: 5f0c…
Postacio-Signature: t=1789466400,v1=3b1f…

{
  "id": "5f0c…",
  "event": "draft.published",
  "created_at": "2026-09-13T10:00:00Z",
  "origin": { "kind": "agent", "name": "Claude" },
  "data": { "draft": { "id": "…", "status": "published", "targets": [ … ] } }
}

id is the delivery id, the same as the header and the same on every retry, so you can ignore one you have already handled. Bodies are at most 32 KiB; a draft too large to fit arrives as { "draft": { "id", "group_id" }, "truncated": true } and you can read it through the API.

Verify the signature

v1 is an HMAC-SHA256 of <t>.<raw body> keyed with your webhook's signing secret (whsec_…, shown once when you add it or rotate it). Verify the raw bytes before parsing the JSON, and reject a timestamp more than 5 minutes old.

Node.js
import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody is the request body exactly as received - verify it before JSON.parse.
// In Express: app.post("/hooks/postacio", express.raw({ type: "application/json" }), handler)
export function verifyPostacioSignature(secret, header, rawBody, toleranceSec = 300) {
  let timestamp = null;
  const signatures = [];
  for (const part of String(header ?? "").split(",")) {
    const [key, value] = part.trim().split("=", 2);
    if (key === "t" && /^\d+$/.test(value ?? "")) timestamp = Number(value);
    if (key === "v1" && value) signatures.push(value);
  }
  if (timestamp === null || signatures.length === 0) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > toleranceSec) return false;

  const expected = Buffer.from(
    createHmac("sha256", secret).update(`${timestamp}.${rawBody}`, "utf8").digest("hex"),
  );
  return signatures.some((signature) => {
    const given = Buffer.from(signature);
    return given.length === expected.length && timingSafeEqual(given, expected);
  });
}

Retries and pausing

  • Any 2xx within 30 seconds counts as delivered. Anything else is a failed attempt.
  • A failed delivery is retried up to 7 times with growing gaps: about 30 seconds, then 1, 2, 4, 8, 16, 32 and 64 minutes.
  • After 25 failed attempts in a row spanning at least 24 hours, the webhook is paused and you get a notification. Fix the endpoint, then choose Resume in Settings.
  • Settings keeps each webhook's recent deliveries with the status your endpoint returned, and can resend any of them.
  • Addresses must be public https:// URLs without a username or password.

Manage webhooks through the API

Tools that subscribe with REST hooks, such as Zapier, can add a webhook when they turn on and remove it when they turn off. A webhook receives events from every group, so this needs a key with access to all groups. Other keys get 403 forbidden.

Shell
curl -X POST https://postacio.com/api/v1/webhooks \
  -H "Authorization: Bearer $POSTACIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://hooks.example.com/postacio", "events": ["draft.published"] }'

curl -X DELETE https://postacio.com/api/v1/webhooks/$WEBHOOK_ID \
  -H "Authorization: Bearer $POSTACIO_API_KEY"

The create answer carries the whsec_ signing secret, once. A retry with the same Idempotency-Key returns the webhook without it. Up to 10 webhooks; PATCH /webhooks/{id} changes one or resumes it with "status": "active". Webhooks added this way show in Settings like any other.

Agent Skill

For a coding agent that runs on your machine and would rather call the API than connect over MCP, Postacio has an Agent Skill. It is a SKILL.md with instructions and a small Node helper, scripts/postacio.mjs, that needs no install beyond Node.

  1. 1
    Copy the agent-skills/postacio folder into your agent's skills folder. For Claude Code, that is ~/.claude/skills/postacio.
  2. 2
    Create an API key (see Get an API key) and set it as POSTACIO_API_KEY in the environment the agent runs in.
  3. 3
    Ask the agent to draft or schedule a post. It reads the skill when the task calls for it.

Everything the skill does goes through the API with that key, with the key's groups and publishing permission.

Questions before you start.

Do AI agents need an API key?

No. Claude, ChatGPT and other MCP clients sign in with your Postacio account over OAuth. Postacio asks which groups the agent may use and whether it may publish (off unless you turn it on), and you can change or revoke that in Settings → API & AI agents. API keys are for your own scripts.

Which plans include the API?

The REST API, webhooks and the MCP server are on every plan, and on the free trial.

Is there an OpenAPI document?

Yes, at /api/v1/openapi.json. It lists every route, parameter and schema, including the options each network takes, and loads in any OpenAPI tool. The API reference at /developers/api is drawn from it.

What are the rate limits?

120 reads and 30 writes a minute for each key or agent, and 300 requests a minute for the account across all of them. A 429 answer says when to try again in Retry-After.

How do I know when a post has gone out?

Add a webhook for draft.published, draft.partially_failed and draft.failed, or read the draft until its publish_state is finished. Each target then carries its permalink or its error.

Build on your own schedule.

Start your free trial, create a key in Settings, and send your first draft.