# Publish

> Publish one post to one or many networks now, and check a post's status.

Source: https://postwire.io/docs/publishing/ · Base URL: https://postwire.io · OpenAPI: https://postwire.io/openapi.json

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](/docs/ai-writing/)) to publish a different native text on each network, and `options` for the [per-network options](/docs/network-options/).

- [`POST /api/post`](#post-api-post): Post (or cross-post) to one or more platforms
- [`GET /api/post/status`](#get-api-post-status): Check the status of a created post

## Post (or cross-post) to one or more platforms

`POST /api/post`

Needs 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. |
| `options` | object (see Network options) | Options per network, keyed by network name. See https://postwire.io/docs/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. |
| `400` | Bad request |
| `401` | Missing or invalid API key |
| `403` | Email not verified, or the key is read-only |

```bash
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"
}'
```

```js
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());
```

```python
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**

```json
{
  "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

`GET /api/post/status`

Needs an API key: `Authorization: Bearer pw_live_…`.

**Parameters**

| Field | Type | Description |
|---|---|---|
| `platform` (required) | query, string |  |
| `id` (required) | query, string |  |

**Responses**

| Status | Means |
|---|---|
| `200` | { platform, id, status } |

```bash
curl "https://postwire.io/api/post/status?platform=instagram&id=igp_5d0c%E2%80%A6" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY"
```

```js
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());
```

```python
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**

```json
{
  "platform": "instagram",
  "id": "igp_5d0c…",
  "status": "processing"
}
```
