docs

REST API reference

Everything in xpost — the dashboard and the MCP connector included — runs on this API. Base URL https://dev.xpost.to/api/v1, machine-readable spec at /api/v1/openapi.json.

Authentication

Every request carries an API key from Dashboard → Agent:

Authorization: Bearer xp_live_YOUR_KEY

Keys carry scopes posts:read (reads and dry-runs), posts:write (posts and media), posts:approve (deciding the queue; human keys only). A missing scope is a 403 that names it. Agent keys can never approve, whatever their scopes say.

Rate limits

Per key: 60 requests/min by default (editable per key), plus an optional daily post cap. Over either limit → 429 with Retry-After and X-RateLimit-* headers. Honor them.

The two responses worth understanding

  • · 201 status: pending_approval — copilot mode held the post for a human. Expected, not an error.
  • · 422 Blocked by project guardrails — the violations array names the rule. Rewrite to comply; never evade.

Endpoints

get/api/v1/postsList recent posts

Last 50 posts for the project with status (pending_approval, scheduled, processing, posted, partial, failed, rejected, draft).

responses

  • 200 Post list
  • 401 Missing or invalid API key
  • 429 Rate limited

post/api/v1/postsCreate a post

Create a post targeting one or more connected accounts. Guardrails run on ALL outbound text (caption, per-platform overrides, X first comment and thread) — a block returns 422 with the violations, never a silent drop. In copilot mode the post is held for human approval: a pending_approval response is the system working, not an error. Omit scheduled_at to publish as soon as it's created/approved. Instagram, TikTok, YouTube, and Pinterest require media; YouTube requires a video. caption may be omitted when no destination insists on one: a story (placement: "stories") prints no caption, so text sent with one is dropped, and X, Instagram and LinkedIn all publish media with no words. Every other platform still requires it, and one such destination in the send brings the requirement back for the whole post — the caption is shared.

body (application/json)

  • caption (string)Required for every destination that prints one — omit it only on a story-only post
  • social_accounts (array, required)Account ids from GET /social-accounts
  • media_ids (array)From POST /media; order = carousel order, first item leads
  • scheduled_at (string)ISO 8601 with Z or a UTC offset; omit to post now (after approval in copilot mode). Past times (beyond a 2-minute grace) are refused.
  • is_draft (boolean)
  • platform_configurations (object)Per-platform options, keyed by platform name (x, instagram, facebook, threads, pinterest, tiktok, youtube, linkedin, bluesky). Common keys: caption (override the main caption for that platform), placement ("reels" | "stories" | "timeline" — instagram/facebook; threads supports reels only; reels need a video, stories need media and print no caption). Pinterest: title, board_ids, link. X: first_comment (auto-replied under the tweet), thread (up to 4 follow-up tweet texts chained under the main tweet), reply_settings, poll. Instagram: first_comment too (max 2200 characters, not on stories) — but only for accounts connected through bundle.social, since Instagram refuses the comment on the other route; asking for one anywhere else is refused at create time, naming the account. Every text field here is guardrail-checked exactly like the caption — including keys not listed above, since anything that reaches a platform is checked. Machine values (placement, board_id, reply_settings, privacy_status and similar enums and ids) are exempt: they are not places a message can hide.
  • skip_signature (boolean)Leave the project's signature off this post. The signature is added to every post by default, including yours.

responses

  • 201 Created — check status; message explains a copilot hold
  • 400 Validation error
  • 401 Missing or invalid API key
  • 403 Missing posts:write scope, or no active plan
  • 422 Blocked by project guardrails — rewrite to comply, never evade
  • 429 Rate limit or the key's daily post cap hit — honor Retry-After

post/api/v1/posts/bulkBulk-create posts (≤100)

Up to 100 posts in one call, as JSON {rows: [...]}, raw text/csv (header caption,accounts[,scheduled_at,media_urls], | separates multiple values inside a cell), or multipart CSV in a file field. Account refs may be ids, platform:username, or a bare username when unambiguous. Every row independently runs the FULL single-post path — guardrails, plan limits, copilot approval. Always read the per-row report: some rows can fail while others succeed; resend only the fixed rows.

body (application/json, also text/csv)

  • rows (array, required)

responses

  • 200 Per-row report — check every row
  • 400 Unparseable body
  • 401 Missing or invalid API key
  • 403 Missing posts:write scope
  • 429 Rate limited or daily post cap hit mid-batch

post/api/v1/posts/{id}/approveApprove or reject a held post

Decide a pending_approval post. Human keys with posts:approve only — agent keys are always 403 here; approval is the product's trust boundary.

parameters

  • id (path, required, string)

body (application/json)

  • decision (string, required)
  • reason (string)

responses

  • 200 Decision applied
  • 401 Missing or invalid API key
  • 403 Agent key, or missing posts:approve scope
  • 404 Post not found in this project
  • 409 Post is not pending approval
  • 429 Rate limited

post/api/v1/mediaUpload media

Attach an image, video or PDF for a post. Either JSON {url} (the server fetches a public https URL) or multipart form-data with a file field. ≤100 MB. Optional: thumbnail_timestamp_ms picks a video's cover frame; alt_text (≤1000 chars) sets an image's accessibility text (applied natively on X and Bluesky). Returns {id, kind} — pass id in create_post media_ids. A PDF (kind: "document") is a LinkedIn document post: one per post, attached on its own, and only to a LinkedIn account whose connection supports documents — create_post refuses anything else and says why.

body (application/json, also multipart/form-data)

  • url (string, required)
  • thumbnail_timestamp_ms (integer)
  • alt_text (string)

responses

  • 200 Stored
  • 400 No file/url, unfetchable URL, or unsupported type
  • 401 Missing or invalid API key
  • 403 Missing posts:write scope
  • 413 Larger than 100 MB
  • 429 Rate limited

get/api/v1/post-resultsDelivery receipts for a post

Per-platform delivery receipts: each targeted account gets its own row with status, retries, the live post URL on success, or error detail on failure. A successful row may also carry note — the post went out, but published less than was attached (X takes 4 images, Facebook won't let an app post photos and video together, a reel is one clip). Check this after publish time and report honestly — including partial failures and any note.

parameters

  • post_id (query, required, string)

responses

  • 200 Receipts
  • 401 Missing or invalid API key
  • 404 Post not found in this project
  • 429 Rate limited

get/api/v1/social-accountsList connected accounts

The agent's account-discovery endpoint: id, platform, username, status per connected account. Use ids as social_accounts when creating posts.

responses

  • 200 Accounts
  • 401 Missing or invalid API key
  • 429 Rate limited

get/api/v1/connection-issuesOpen connection-health issues

What's broken right now: dead logins, expiring tokens, unreachable accounts — detected proactively by the health checker.

responses

  • 200 Open issues
  • 401 Missing or invalid API key
  • 429 Rate limited

post/api/v1/connection-issuesRun a connection health check now

Trigger an immediate health check of the project's connected accounts (bypasses the scheduler's throttle). Returns a summary plus the open-issue list.

responses

  • 200 Check ran
  • 401 Missing or invalid API key
  • 429 Rate limited

get/api/v1/metricsEngagement metrics for one post

Per-delivery engagement (views, likes, comments, shares, reach, saves, engagement) with the live URL. A null metric means the platform doesn't report it — not zero. Empty data = not synced yet (metrics refresh roughly every 6 h).

parameters

  • post_id (query, required, string)

responses

  • 200 Metrics
  • 401 Missing or invalid API key
  • 404 Post not found in this project
  • 429 Rate limited

get/api/v1/insightsProject engagement roll-up

Post/delivery counts, metric totals, and a per-platform breakdown over the window. "How are we doing?" in one call. Optionally sliced to one platform and/or one format (feed post / reel / story).

parameters

  • since_days (query, integer, default 30)
  • platform (query, string)
  • format (query, feed | reel | story)

responses

  • 200 Roll-up
  • 401 Missing or invalid API key
  • 429 Rate limited

get/api/v1/top-postsBest-performing deliveries

Deliveries ranked by a metric over the window — study these before drafting to learn what works for this audience.

parameters

  • metric (query, engagement | views | likes | comments | shares | reach | saves, default engagement)
  • since_days (query, integer, default 30)
  • limit (query, integer, default 10)
  • platform (query, string)
  • format (query, feed | reel | story)

responses

  • 200 Ranked deliveries
  • 401 Missing or invalid API key
  • 429 Rate limited

get/api/v1/best-timesBest posting times

Weekday × hour (UTC) slots ranked by average engagement from the project's OWN past results. Use when choosing scheduled_at; each slot carries its sample count — few samples, weak signal.

parameters

  • platform (query, string)
  • format (query, feed | reel | story)
  • since_days (query, integer, default 90)

responses

  • 200 Slots
  • 401 Missing or invalid API key
  • 429 Rate limited

post/api/v1/guardrails/checkDry-run guardrails on a draft

Check a caption against the project's brand guardrails (banned words/topics, tone, link policy, posting cap) WITHOUT creating a post. Fix the draft before create_post instead of burning a rejection.

body (application/json)

  • caption (string, required)

responses

  • 200 Verdict
  • 400 Missing caption
  • 401 Missing or invalid API key
  • 429 Rate limited

get/api/v1/openapi.jsonThis spec

The machine-readable OpenAPI 3.1 description of the API. Public, no auth.

responses

  • 200 OpenAPI document