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 (the timezone of 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 400 no_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.

    Send an Idempotency-Key header. A retried request carrying the same key returns the original post (200) instead of creating a second one — whatever its body. Without it, a network timeout on your side can mean the same video going out twice.

    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.

    A created post is not a published post. 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

    GET/v1/postsPosts with their per-network state
    POST/v1/postsCreate, schedule, queue or publish
    POST/v1/platforms/validateDry run against each network
    GET/v1/posts/{id}One post
    PATCH/v1/posts/{id}Change content, files, time or networks
    DELETE/v1/posts/{id}Remove it here
    POST/v1/posts/{id}/retryRetry the networks that failed
    POST/v1/posts/metrics/refreshRefresh the last fortnight's figures

    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.

    POST/v1/posts/{id}/targets/{targetId}/metricsFetch its figures now
    GET/v1/posts/{id}/targets/{targetId}/metrics/historyEvery reading, oldest first; ?since=
    GET/v1/posts/{id}/targets/{targetId}/insightsYouTube retention and traffic sources
    GET/v1/posts/{id}/targets/{targetId}/commentsComments, read live
    POST/v1/posts/{id}/targets/{targetId}/comments/{commentId}Act on one comment
    PATCH/v1/posts/{id}/targets/{targetId}/remoteEdit the live post
    DELETE/v1/posts/{id}/targets/{targetId}/remoteDelete it from the network
    PATCH/v1/posts/{id}/targets/{targetId}/accountPoint it at another account

    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

    GET/v1/platformsLimits and abilities per network; no key
    GET/v1/oauth/accountsConnected accounts, with their zone
    GET/v1/oauth/accounts/{id}/boardsPinterest boards
    GET/v1/oauth/accounts/{id}/youtube/playlistsYouTube playlists
    POST/v1/oauth/accounts/{id}/youtube/playlistsA new playlist: { "title", "privacy" }
    GET/v1/oauth/accounts/{id}/tiktok/creatorWhat this TikTok creator may use
    GET/v1/oauth/accounts/{id}/mentions?q=Handles to mention

    Accounts are connected and disconnected in the app, not over the API.

    AI

    POST/v1/ai/captionsA caption per network
    POST/v1/ai/reviewWhat its figures say to change
    GET/v1/ai/review/{targetId}The last review, free to read

    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.