docs

MCP tool

create_post

Create a post

Creates or changes something · and, like everything an agent creates, it waits for a human in the approval queue.

What it does

Create a social media post targeting one or more connected accounts. In copilot mode the post is held for human approval before publishing — the response says so; do not treat 'pending_approval' as an error. It comes back with approval_link — a ready-made Markdown link to a page showing this exact post with Approve and Reject buttons, no login needed. Print that string EXACTLY as it is, on its own line at the end of your reply: it is the last step of the job, not an extra. It is already written — do not unwrap it, do not print the address inside it, and do not describe it. Omit scheduled_at to post as soon as it can go — the reply then says scheduled_at: null, which means the post has NO time, not that the time is unknown. publishes says what it is waiting for: "on_approval" (a person's yes — tell the user "as soon as you approve it"), or "now" (nothing at all, it is already on its way). A post with a real slot answers "at_time" and the slot is in scheduled_at. Never invent a time to report. Instagram/TikTok/YouTube/Pinterest require media_ids (see upload_media). is_draft: true files it as a DRAFT instead — a scratchpad post that publishes nowhere and skips the approval queue, for when the user asked you to prepare options rather than to post. It has no approval link; the user sends it from its page in xpost when ready. update_post can keep editing it (it stays a draft); delete_post clears the versions they didn't pick. A project with no plan can still send ONE post, free, and the reply says free_first_post: true when this was it. Say that this one was free and that posting beyond it needs a plan — they are otherwise never told, and the offer was made so they could see it work first. plans_url is a page explaining the plans; offer it only if they ask what a plan costs. Never pitch an upgrade unprompted.

Arguments

NameTypeRequiredWhat it is
captionstringNoThe post text. Required unless no destination insists on one: a story (placement "stories") prints no caption, so text sent with one is dropped, and X, Instagram and LinkedIn publish media with no words. Every other platform still needs it, and one such destination brings the requirement back for the post.
accountsarrayNoWhere it posts: the ref of each account from list_accounts — "x:someone", "instagram:someone". SEND THE REF, not the uuid sitting beside it. A person is shown this exact request and has to approve it before anything is created; a uuid asks them to approve a destination they cannot see, and the ref is the same instruction they can check. (Where two accounts share a username on one platform, that account's ref IS its uuid — send it as given and it works.) A reference matching nothing refuses the whole post rather than posting to the ones that did match.
social_account_idsarrayNoDeprecated name for accounts — send accounts.
media_idsarrayNoMedia ids from upload_media; first is the carousel lead
scheduled_atstringNoISO 8601 time to publish, Z or a UTC offset (optional)
is_draftbooleanNoFile as a draft: saved, visible in xpost, publishing nowhere until a person sends it. Use when asked to prepare or draft rather than to post — quieter than the approval queue, since nothing sits waiting for a yes/no.
platform_configurationsrecordNoPer-platform options, keyed by platform name. caption overrides the main caption for that platform; placement posts to a different surface ("reels"/"stories") where the account offers one. WHICH keys each account takes — with accepted values, ceilings, placement gates and the per-connection caveats (first comments, X threads, Pinterest boards, reel music) — is exactly what get_posting_rules answers, per account; read it first rather than guessing. A key it doesn't list for an account is refused or loses the delivery. Every text field here is guardrail-checked like the caption. Example: {"instagram": {"placement": "stories"}}
idempotency_keystringNoA string of your own that identifies THIS ONE REQUEST — reuse it only when you are re-sending the very same call. A call that times out may still have created the post; with a key the retry returns that post instead of making a second. Make it unique to the request, NOT to the subject: a key built from a topic and a date ("eclipse-temp-drop-2026-08-13") collides with any other post about that subject that day, and the second one is silently not created — the caller gets the first post back instead, which may be hours old, already published, or written by somebody else. Add something that only this request has. When a reply comes back with repeated: true it is telling you exactly that: read its caption and created_at, and if they are not yours, send again with a different key rather than reporting the old post as new.
skip_signaturebooleanNoLeave the project's signature off this post. The signature — a sign-off, a handle, a link — is a project setting the owner already made, and it is added to every post including yours. Never ask the user about it and never judge whether it fits: leave this unset unless the user has themselves said, in words, to omit the signature from this post.

Calling it

Any MCP client calls this by name once the connector is added. The address is the same for every tool:

https://xpost.to/api/mcp

A client that has signed in sends the tool name and its arguments:

{
  "name": "create_post",
  "arguments": {
    "caption": "…",
    "accounts": []
  }
}

The other tools

Back to the MCP connector · What the MCP server is · Connect your assistant