API reference
Posts: create, schedule, read, change and delete
A post in the Preflight API is one body of text and files with a list of targets — the networks and accounts it goes to — created with POST /v1/posts and delivered to each target independently. It is scheduled for an instant, for a wall-clock time in each channel's zone, into each channel's queue, published at once or saved as a draft; read back with its per-network status, links and figures; and changed or deleted while it waits.
Creating a post
One call, as many networks as you name. Each is delivered independently: one failing does not hold up the rest.
POST /v1/posts
Idempotency-Key: 7d0c2c9e-…
{ "content": "This week's mix is up.",
"mediaIds": ["9f3c…"],
"scheduledAt": "2026-10-05T09:00:00Z",
"targets": [
{ "platform": "youtube",
"options": { "title": "Weekly Mix #14", "privacy": "public" } },
{ "platform": "facebook" }
] }
→ 201 { "id": "…", "status": "scheduled", "targets": [ … ], "issues": [ … ] }When it goes out
Exactly one of these:
scheduledAt— an ISO instant, the same moment on every network. A time already past publishes at once.scheduledLocal—"2026-10-05T14:00", a wall-clock time read in each channel's own zone (thetimezoneof each account), so two in the afternoon in New York and in Prague. A channel without a zone of its own uses the workspace's, and Europe/Prague when neither is set."queue": true— each channel takes its own next free slot from its posting schedule; a channel without one is refused with 400no_slots."publishNow": true— straight out. It wins over a time sent with it."saveAsDraft": true— parked without a time. A draft may be incomplete: the networks' rules and the plan's allowance are checked when it is scheduled.
The rest of the body
content (up to 64,000 characters; each network's own limit is checked), mediaIds (1–35 file ids, in order; mediaId still works for one), altTexts and linkUrl — an http(s) link used by Facebook text posts, LinkedIn's article card and as a Pinterest pin's destination. Each target names a platform, an accountId and its options — every field a network takes is on per-network options; how a file gets its id is on media uploads.
The answer is 201 with the post, its targets and issues — warnings that did not stop it, such as files a network will not take. Anything that would stop a network is a 422 validation_failed with the same issues, and nothing is created.
Several accounts on one network
Name the network once per account. Each target is delivered on its own and carries its own status, link and figures, so one Page failing does not affect the other four. Without accountId the first connected account of that network is used.
"targets": [
{ "platform": "facebook", "accountId": "b4b5…" },
{ "platform": "facebook", "accountId": "7c19…" }
]Account ids, and each account's timezone, come from GET /v1/oauth/accounts (posts:read).
Different text for one network
caption in a target's options replaces content for that network only: a shorter version for Bluesky, one without hashtags for LinkedIn. It is checked against that network's own limit. An empty caption falls back to content.
"targets": [
{ "platform": "facebook" },
{ "platform": "bluesky", "options": { "caption": "Weekly Mix #14 is up — deep house, live from Brno." } }
]Carousels and alt text
Several mediaIds make a carousel or an album, up to each network's maxMediaPerPost: Instagram, Facebook, Telegram and Discord 10, Threads and LinkedIn 20, Tumblr 30, TikTok 35 pictures, Bluesky and Mastodon 4, every other network one. Files past the limit are left off with a warning. TikTok takes pictures or one video, never both; Facebook builds an album from pictures only.
altTexts describes each picture for people using screen readers, by position in mediaIds, null for none, up to 1,000 characters. It is kept on this post's use of the file, so the same picture can say different things in different posts. Instagram, Facebook, Threads, Bluesky, Mastodon, LinkedIn, Tumblr, Pinterest (500 characters) and Discord receive it — the capability sheet marks them with supportsAltText.
{ "content": "Opening night.",
"mediaIds": ["9f3c…", "2b7a…"],
"altTexts": ["The band on a small stage under red lights", null],
"targets": [{ "platform": "instagram" }, { "platform": "bluesky" }] }A thread
thread in a target's options is the rest of a thread: each entry is posted as a reply under the one before, after content goes out first. Threads, X, Mastodon and Bluesky post it; each part is checked against that network's own limit. A network without threads gets only the first post, and the dry run says so.
{ "platform": "threads", "options": { "thread": ["Part two.", "Part three."] } }Writing for each network
POST /v1/ai/captions (posts:write) with content (up to 8,000 characters) and the platforms it is for returns a caption per network and a few hashtags, to use as each target's caption. It writes with Claude Sonnet 5, keeps the original language and every fact, name, number and link, and aims inside each network's limit — the post itself is still checked. Pro includes 300 rewrites a month and Studio 1,500; other plans get 402 ai_not_in_plan. 20 calls a minute.
POST /v1/ai/captions
{ "content": "Weekly Mix #14 is up: two hours of deep house, recorded live in Brno.",
"platforms": ["linkedin", "bluesky"] }
→ { "captions": {
"linkedin": "Weekly Mix #14 is out — two hours of deep house, recorded live in Brno.",
"bluesky": "Weekly Mix #14 is up. Two hours of deep house, live from Brno." },
"hashtags": ["deephouse", "djset", "brno"],
"left": 297, "allowed": 300 }Check before you commit
POST /v1/platforms/validate takes the same content, mediaIds, altTexts and targets and returns { ok, issues } — a caption too long for X, a vertical video TikTok requires, a file id that is not in this workspace, more files than a network takes — without creating anything. It checks more than creating does: YouTube subtitle files, tags and thumbnails, and hashtag counts. Each issue has a severity of error or warning; errors block publishing.
Reading posts
GET /v1/posts returns { data, nextBefore }, newest first by when each post went out (or will). Without parameters: every draft and scheduled post whatever its date, and what went out in the last 120 days. Parameters: limit (default 200, up to 500), days (the window, up to 730), from and to (an ISO window instead; both or neither), and before — pass the nextBefore of one page to get the next.
{ "id": "…", "status": "done", "content": "…", "linkUrl": null,
"scheduledAt": "…", "createdAt": "…",
"mediaIds": ["9f3c…", "2b7a…"], "altTexts": ["…", null],
"mediaItems": [ { "id": "9f3c…", "kind": "image", "status": "ready", "url": "https://…",
"altText": "…", "durationSeconds": null, "position": 0 }, … ],
"media": { … the first of them, for a thumbnail … },
"targets": [
{ "id": "…", "platform": "instagram", "accountId": "…",
"status": "published", "remoteId": "…", "remoteUrl": "https://instagram.com/p/…",
"publishedAt": "…", "error": null, "warning": null, "attempts": 1, "nextAttemptAt": null,
"canDeleteRemote": true, "canEditRemote": false,
"metrics": { "views": 1200, "likes": 80, "comments": 4, "shares": 2, "saves": 9,
"reach": 950, "impressions": 1300, "fetchedAt": "…" } } ] }A post is draft, scheduled, publishing or done (every network finished, failures included). Each target is pending, publishing, published, failed (with error; a retry that is coming shows in nextAttemptAt) or skipped (deleted from the network). While remotePending is true the network is still processing it and remoteUrl is not known yet.
POST /v1/posts returns as soon as the work is queued. Read targets[].status, or subscribe to a webhook, to find out what actually happened.Changing a post
PATCH /v1/posts/{id} takes content, linkUrl, mediaIds and altTexts (replacing the files; an empty list removes them), scheduledAt, publishNow and targets. A post already publishing or done is refused with 409 already_started; a scheduled post's networks can change until any of them has started (targets_locked). Giving a draft a time runs the same checks as creating a post.
A post the client has approved (approvalState is approved) keeps that approval when only its time moves. Any other change to what goes out sets it back to changes — it will not publish until a new approval link is approved — and the answer carries approvalNeedsRenewal: true. Send "keepApproval": true for a small fix the approval should survive.
DELETE /v1/posts/{id} removes the post here, and a video waiting on YouTube with it. What has already been published stays on the networks — delete that per target with DELETE …/targets/{targetId}/remote.
Endpoints
Posts
One network's copy of a post
A post has one target per network account, each with its own id, remote id and figures. Not every network allows every action; each target says what it can with canDeleteRemote and canEditRemote.
A comment action is { "action": "reply", "text": "…" }, { "action": "edit", "text": "…" } (your own comment, on YouTube) or hide, unhide, like, unlike, delete; the comments listing says which the network allows in can. Editing the live post takes content (and title on YouTube) on Facebook, YouTube, Mastodon, Telegram and Discord. Deleting marks the target skipped and keeps the record; TikTok, a Facebook Page story and an Instagram account connected directly rather than through a Facebook Page cannot be deleted from through any API. Moving a target takes { "accountId": "…" } of the same network.
Accounts and capabilities
Accounts are connected and disconnected in the app, not over the API.
AI
A review takes { "targetId": "…" } and is included 500 times a month on Pro and 2,500 on Studio.
Questions: info@preflight.social. The same API as a schema: openapi.json (OpenAPI 3.1).
Write it once. Let the rules be our problem.
Preflight checks every post against each network's rules before it leaves, then publishes it to twelve networks. Free plan, no card.