Publish
Publish one post to one or many networks now, and check a post's status.
One request publishes to every network you name, in parallel. Each network gets its own result, so one failing never fails the others. posted counts the networks that went out. Send per_platform drafts (from AI writing) to publish a different native text on each network, and options for the per-network options.
POST /api/post: Post (or cross-post) to one or more platformsGET /api/post/status: Check the status of a created post
Post (or cross-post) to one or more platforms
/api/postNeeds an API key: Authorization: Bearer pw_live_….
Body (JSON)
| Field | Type | Description |
|---|---|---|
platform | string | Single platform; or use 'platforms'. |
platforms | array of string | Cross-post to many at once, in parallel. |
text | string | Caption / description. |
title | string | Title (used by YouTube). |
video_url | string | URL of the video to post (required for YouTube; TikTok takes a video or photos). |
photo_url | string | URL of one image. TikTok posts it as a photo post. Instagram and TikTok take JPEG (TikTok also WebP): PostWire converts PNG, WebP, AVIF and still GIF to JPEG automatically; animated GIF and HEIC are refused. |
privacy | string | e.g. private | public | unlisted (YouTube). |
media | array of object | Several images (or images and videos) in one post, in order. Instagram: a carousel of up to 10 (images and MP4 videos). TikTok: a photo post of up to 35 images. PNG (and WebP, AVIF, still GIF) is converted to JPEG automatically for Instagram and TikTok. Bluesky: up to 4 images. Mastodon: up to 4. LinkedIn: up to 20 images, or one document (PDF/PPT/DOC). Networks that take one item get the first, and the result says so. Also accepted inside a per_platform draft. Up to 35 items. |
media[].url required | string | Public https URL of the file. |
media[].type | string | One of: image, video, document. Default "image". |
media[].alt | string | Alt text (Bluesky, Mastodon, LinkedIn). |
media[].title | string | Document title (LinkedIn). |
photo_urls | array of string | Shorthand for media: a list of image URLs. |
per_platform | object | Per-network overrides, keyed by network name (e.g. {"tiktok":{"text":"…"},"youtube":{"title":"…","text":"…"}}). Each value may carry text, title, video_url, photo_url, media and network options. X also takes thread: ["…", "…"] — the posts of a thread after text, in order (at most 25), each published as a reply to the one before it; each at most 280 characters as X counts them (a link counts 23, an emoji 2), and an empty, non-text or over-long post, or a 26th, is refused with code bad_x_thread before anything goes out. Media goes on the first post only. Every post of a thread is an X post: it uses X credits by the same rule (1, or 10 with a link) and counts toward the daily X limit; the whole thread is reserved before the first post is sent, so a thread without enough credits is refused whole. |
per_platform.x | object | |
options | object (see Network options) | Options per network, keyed by network name. See Network options. |
brand_id | string | Brand whose connected accounts publish this post (GET /api/brands). Defaults to the account's only brand. |
idempotency_key | string | Send the same key on a retry and the post is not published twice. |
Responses
| Status | Means |
|---|---|
200 | One result per network. Check `posted` and each `results[].ok`. Each result carries account { platform, handle, display_name, brand_id, brand, label } and published_to names every account reached. With the same network in more than one brand and no brand_id: 409 ambiguous_brand, nothing published. On a plan with a networks-per-post limit (Free: 2) the first networks that pass their checks are published and the others come back held (code networks_per_post), listed in held_networks with held_message and an upgrade link: a partial publish is a 200 that says what it held back. |
202 | 202 = held for approval (the brand's approval rules or a limit on this key): nothing was published. { ok: true, posted: 0, status: "pending_approval", id (the post's PostWire id, for GET /api/post/status), approval_id, approval: { reasons, notified: [{ to (masked), role, via, sent }], review_url (the dashboard; sign-in needed), who_can_approve, publishes }, results: [{ platform, ok: false, status: "pending_approval", code: "pending_approval", account }], message_for_user }. Only a person approves it: the emailed link or a signed-in dashboard session, never an API key or an AI connector. |
400 | Bad request. bad_x_thread: an X thread (per_platform.x.thread) or reply (options.x.reply) post is empty, not text or over 280 characters as X counts them, or the thread has more than 25 posts — nothing was published to any network. |
401 | Missing or invalid API key |
403 | Email not verified, or the key is read-only. 403 also: key_network_not_allowed, key_brand_not_allowed, key_monthly_cap_reached (limits set on this API key or AI app; nothing was published), plan_feature. 503 governance_unavailable: the approval rules could not be checked, nothing was published. |
curl -X POST "https://postwire.io/api/post" \
-H "Authorization: Bearer $POSTWIRE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"platforms": [
"bluesky",
"linkedin",
"telegram"
],
"text": "Our sourdough now proofs for 36 hours. Here is what changed in the crumb.",
"idempotency_key": "bakery-2026-10-03-sourdough"
}'const res = await fetch("https://postwire.io/api/post", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
"platforms": [
"bluesky",
"linkedin",
"telegram"
],
"text": "Our sourdough now proofs for 36 hours. Here is what changed in the crumb.",
"idempotency_key": "bakery-2026-10-03-sourdough"
}),
});
console.log(res.status, await res.json());import os, requests
r = requests.post(
"https://postwire.io/api/post",
headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
json={
"platforms": ["bluesky", "linkedin", "telegram"],
"text": "Our sourdough now proofs for 36 hours. Here is what changed in the crumb.",
"idempotency_key": "bakery-2026-10-03-sourdough",
},
)
print(r.status_code, r.json())Example response
{
"posted": 3,
"published_to": "Published to @bakery.bsky.social (Bakery) on bluesky, Ana Ruiz (Bakery) on linkedin, chat @bakerynews (Bakery) on telegram.",
"results": [
{
"ok": true,
"platform": "bluesky",
"id": "at://did:plc:4x2…/app.bsky.feed.post/3l7…",
"url": "https://bsky.app/profile/bakery.bsky.social/post/3l7…",
"account": {
"platform": "bluesky",
"handle": "bakery.bsky.social",
"brand": "Bakery",
"label": "@bakery.bsky.social (Bakery)"
}
},
{
"ok": true,
"platform": "linkedin",
"id": "urn:li:share:72…",
"url": "https://www.linkedin.com/feed/update/urn:li:share:72…"
},
{
"ok": true,
"platform": "telegram",
"id": "412",
"url": "https://t.me/bakerynews/412"
}
]
}Each result also carries account, the connected account it went to. The second and third are shortened here.
Check the status of a created post
/api/post/statusWith platform and the network's post id: that post's status and link. With the id PostWire returned for a post that waits for approval or is scheduled (a uuid; platform can be omitted): status pending_approval (with the approval: who was asked, review_url), rejected (with the reason and who decided), scheduled (approved_by when it was approved), or the post's status and each network's result once it went out.
Needs an API key: Authorization: Bearer pw_live_….
Parameters
| Field | Type | Description |
|---|---|---|
platform | query, string | The network. Can be omitted when id is PostWire's id for a waiting or scheduled post. |
id required | query, string |
Responses
| Status | Means |
|---|---|
200 | { platform, id, status } |
curl "https://postwire.io/api/post/status?platform=instagram&id=igp_5d0c%E2%80%A6" \
-H "Authorization: Bearer $POSTWIRE_API_KEY"const res = await fetch("https://postwire.io/api/post/status?platform=instagram&id=igp_5d0c%E2%80%A6", {
method: "GET",
headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}` },
});
console.log(res.status, await res.json());import os, requests
r = requests.get(
"https://postwire.io/api/post/status?platform=instagram&id=igp_5d0c%E2%80%A6",
headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
)
print(r.status_code, r.json())Example response
{
"platform": "instagram",
"id": "igp_5d0c…",
"status": "processing"
}