# Schedule and queue

> Queue posts for a time or for the next free slot, edit and cancel them, and fill a week from one topic.

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

A scheduled post is checked when you queue it, not when it runs: a network that is not connected, a missing video or a dead media link is refused now. The worker publishes due posts every 5 minutes, through the same path as an immediate post. On the Free plan 3 posts wait in the queue at a time; one more is saved as held (202) and goes out after an upgrade.

**Queue slots.** Send `run_at: "next_slot"` instead of a time and the post takes its brand's next free slot. The rule, exactly: each network of a brand has weekly slots (days and times, in the brand's timezone); a post goes to the earliest slot that **every** one of its networks has and that **none** of them already uses for another waiting post of that brand; when its networks share no slot, to the earliest slot of any of them that is free for all. The answer says which time it chose (`slot.local`). Defaults: most networks every day at 09:00, 12:00 and 17:00, LinkedIn on weekdays, TikTok at 12:00, 17:00 and 20:00, YouTube at 12:00 and 17:00. Change them per brand with `PUT /api/brands/{id}/slots` or in the dashboard (Queue → Queue slots). Preview with `GET /api/schedule/next-slot`.

- [`POST /api/schedule`](#post-api-schedule): Queue a post for a later time
- [`GET /api/schedule/next-slot`](#get-api-schedule-next-slot): Where run_at "next_slot" would put a post now (nothing is queued)
- [`GET /api/brands/{id}/slots`](#get-api-brands-id-slots): A brand's queue slots: weekly times per network, and the timezone
- [`PUT /api/brands/{id}/slots`](#put-api-brands-id-slots): Set a brand's queue slots and timezone
- [`GET /api/schedule`](#get-api-schedule): List queued posts
- [`GET /api/schedule/{id}`](#get-api-schedule-id): One scheduled post with each network's result
- [`PATCH /api/schedule/{id}`](#patch-api-schedule-id): Move or edit a queued post
- [`DELETE /api/schedule/{id}`](#delete-api-schedule-id): Cancel a queued or held post
- [`POST /api/week`](#post-api-week): Write and queue a week of posts from one topic

## Queue a post for a later time

`POST /api/schedule`

Rules are checked NOW, not at send time: a disconnected network or missing media is rejected while you build the flow rather than silently failing overnight. The Free plan keeps 3 scheduled posts waiting at a time: past that the post is not refused but saved as held (202, code queue_limit, with an upgrade link) and scheduled automatically when the plan is upgraded; it is never published on Free and can be canceled. On Free only the first 2 networks of a post are published when it runs; the answer names the others in held_networks. run_at "next_slot" puts the post in the brand's next free queue slot (GET/PUT /api/brands/{id}/slots); the Free queue cap applies the same way.

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

**Body (JSON)**

| Field | Type | Description |
|---|---|---|
| `platforms` (required) | array of string | One of: tiktok, instagram, youtube, linkedin, facebook, bluesky, mastodon, telegram, discord. |
| `text` | string |  |
| `run_at` (required) | string | When to publish: an ISO 8601 time in the future (up to 365 days), or "next_slot" for the brand's next free queue slot. A post goes to the earliest slot that every one of its networks has and that none of them already uses for another waiting (queued, held or publishing) post of the same brand. When its networks share no slot, it goes to the earliest slot of any of them that is free for all. Slots less than 2 minutes away are skipped; the search looks 28 days ahead. The answer then carries slot { run_at, timezone, local, shared }. |
| `video_url` | string (uri) |  |
| `brand_id` | string (uuid) |  |
| `label` | string | Up to 120 characters. |
| `timezone` | string | With run_at next_slot: the IANA timezone to read the slots in when the brand has none saved (e.g. America/Lima). Default UTC. Not stored with the post. |

**Responses**

| Status | Means |
|---|---|
| `200` | { ok, scheduled: {...}, targets[], message, held_networks?, held_message?, upgrade? } |
| `202` | Saved as held, not scheduled: the plan's queue is full. { ok: true, held: true, code: "queue_limit", scheduled: { status: "held", ... }, scheduled_queue: { used, limit, held }, message, upgrade, upgrade_url, upgrade_message } |
| `400` | run_at in the past, network not connected, media required, or the 500-post sanity ceiling |
| `409` | code no_free_slot (run_at next_slot found no free slot in 28 days), or ambiguous_brand |

```bash
curl -X POST "https://postwire.io/api/schedule" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "platforms": [
    "linkedin",
    "bluesky"
  ],
  "text": "Doors open at 7 tomorrow. First 20 loaves are rye.",
  "run_at": "next_slot",
  "timezone": "America/Lima",
  "label": "Monday rye"
}'
```

```js
const res = await fetch("https://postwire.io/api/schedule", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    "platforms": [
      "linkedin",
      "bluesky"
    ],
    "text": "Doors open at 7 tomorrow. First 20 loaves are rye.",
    "run_at": "next_slot",
    "timezone": "America/Lima",
    "label": "Monday rye"
  }),
});
console.log(res.status, await res.json());
```

```python
import os, requests

r = requests.post(
    "https://postwire.io/api/schedule",
    headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
    json={
        "platforms": ["linkedin", "bluesky"],
        "text": "Doors open at 7 tomorrow. First 20 loaves are rye.",
        "run_at": "next_slot",
        "timezone": "America/Lima",
        "label": "Monday rye",
    },
)
print(r.status_code, r.json())
```

**Example response**

```json
{
  "ok": true,
  "scheduled": {
    "id": "7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f",
    "run_at": "2026-10-05T14:00:00+00:00",
    "status": "queued",
    "platforms": [
      "linkedin",
      "bluesky"
    ],
    "label": "Monday rye"
  },
  "slot": {
    "run_at": "2026-10-05T14:00:00.000Z",
    "timezone": "America/Lima",
    "local": "Mon, Oct 5, 09:00",
    "shared": true
  },
  "message": "Scheduled for 2026-10-05T14:00:00.000Z (the next free slot: Mon, Oct 5, 09:00, America/Lima) to Ana Ruiz (Bakery) on linkedin, @bakery.bsky.social (Bakery) on bluesky."
}
```

Or send an exact time: `"run_at": "2026-10-06T13:00:00Z"`. Response abridged (`targets` left out).

## Where run_at "next_slot" would put a post now (nothing is queued)

`GET /api/schedule/next-slot`

A post goes to the earliest slot that every one of its networks has and that none of them already uses for another waiting (queued, held or publishing) post of the same brand. When its networks share no slot, it goes to the earliest slot of any of them that is free for all. Slots less than 2 minutes away are skipped; the search looks 28 days ahead.

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

**Parameters**

| Field | Type | Description |
|---|---|---|
| `platforms` (required) | query, string | Comma-separated networks, e.g. linkedin,bluesky. |
| `brand_id` | query, string (uuid) |  |
| `timezone` | query, string | IANA timezone, used when the brand has none saved. |

**Responses**

| Status | Means |
|---|---|
| `200` | { brand_id, platforms, run_at, timezone, local, shared } |
| `400` | platforms missing or unknown, bad_timezone |
| `409` | no_free_slot |

```bash
curl "https://postwire.io/api/schedule/next-slot?platforms=linkedin%2Cbluesky&timezone=America%2FLima" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY"
```

```js
const res = await fetch("https://postwire.io/api/schedule/next-slot?platforms=linkedin%2Cbluesky&timezone=America%2FLima", {
  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/schedule/next-slot?platforms=linkedin%2Cbluesky&timezone=America%2FLima",
    headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
)
print(r.status_code, r.json())
```

**Example response**

```json
{
  "brand_id": "b3f1c2d4-8a9e-4f6b-a1c2-3d4e5f6a7b8c",
  "platforms": [
    "linkedin",
    "bluesky"
  ],
  "run_at": "2026-10-05T14:00:00.000Z",
  "timezone": "America/Lima",
  "local": "Mon, Oct 5, 09:00",
  "shared": true
}
```

## A brand's queue slots: weekly times per network, and the timezone

`GET /api/brands/{id}/slots`

Each network of the brand: its saved slots, or the defaults (most networks every day at 09:00, 12:00 and 17:00; LinkedIn weekdays; TikTok 12:00, 17:00 and 20:00; YouTube 12:00 and 17:00). custom lists the networks with their own.

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

**Parameters**

| Field | Type | Description |
|---|---|---|
| `id` (required) | path, string (uuid) |  |
| `timezone` | query, string | Shown when the brand has none saved. |

**Responses**

| Status | Means |
|---|---|
| `200` | { brand_id, name, timezone, timezone_saved, slots: { <network>: { mon: ["09:00"], … } }, custom: [], defaults, rule } |
| `404` | brand_not_found |

```bash
curl "https://postwire.io/api/brands/b3f1c2d4-8a9e-4f6b-a1c2-3d4e5f6a7b8c/slots" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY"
```

```js
const res = await fetch("https://postwire.io/api/brands/b3f1c2d4-8a9e-4f6b-a1c2-3d4e5f6a7b8c/slots", {
  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/brands/b3f1c2d4-8a9e-4f6b-a1c2-3d4e5f6a7b8c/slots",
    headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
)
print(r.status_code, r.json())
```

## Set a brand's queue slots and timezone

`PUT /api/brands/{id}/slots`

Replaces the slots of each network named; null puts a network back on the defaults. Networks not named keep theirs.

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

**Parameters**

| Field | Type | Description |
|---|---|---|
| `id` (required) | path, string (uuid) |  |

**Body (JSON)**

| Field | Type | Description |
|---|---|---|
| `timezone` | string | IANA name, e.g. America/Lima. null clears it. |
| `slots` | object | Keyed by network. |
| `slots.tiktok` | object | Days to times, 24-hour HH:MM in the brand's timezone. |
| `slots.instagram` | object | Days to times, 24-hour HH:MM in the brand's timezone. |
| `slots.youtube` | object | Days to times, 24-hour HH:MM in the brand's timezone. |
| `slots.linkedin` | object | Days to times, 24-hour HH:MM in the brand's timezone. |
| `slots.facebook` | object | Days to times, 24-hour HH:MM in the brand's timezone. |
| `slots.bluesky` | object | Days to times, 24-hour HH:MM in the brand's timezone. |
| `slots.mastodon` | object | Days to times, 24-hour HH:MM in the brand's timezone. |
| `slots.telegram` | object | Days to times, 24-hour HH:MM in the brand's timezone. |
| `slots.discord` | object | Days to times, 24-hour HH:MM in the brand's timezone. |

**Responses**

| Status | Means |
|---|---|
| `200` | { ok, …the same body as GET } |
| `400` | bad_timezone, or a bad day, time or network (bad_request) |
| `404` | brand_not_found |
| `503` | slots_unavailable: slots cannot be saved on this server yet (the defaults still apply) |

```bash
curl -X PUT "https://postwire.io/api/brands/b3f1c2d4-8a9e-4f6b-a1c2-3d4e5f6a7b8c/slots" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "timezone": "America/Lima",
  "slots": {
    "linkedin": {
      "mon": [
        "08:30",
        "12:00"
      ],
      "tue": [
        "08:30",
        "12:00"
      ],
      "wed": [
        "08:30"
      ],
      "thu": [
        "08:30"
      ],
      "fri": [
        "08:30"
      ],
      "sat": [],
      "sun": []
    },
    "tiktok": null
  }
}'
```

```js
const res = await fetch("https://postwire.io/api/brands/b3f1c2d4-8a9e-4f6b-a1c2-3d4e5f6a7b8c/slots", {
  method: "PUT",
  headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    "timezone": "America/Lima",
    "slots": {
      "linkedin": {
        "mon": [
          "08:30",
          "12:00"
        ],
        "tue": [
          "08:30",
          "12:00"
        ],
        "wed": [
          "08:30"
        ],
        "thu": [
          "08:30"
        ],
        "fri": [
          "08:30"
        ],
        "sat": [],
        "sun": []
      },
      "tiktok": null
    }
  }),
});
console.log(res.status, await res.json());
```

```python
import os, requests

r = requests.put(
    "https://postwire.io/api/brands/b3f1c2d4-8a9e-4f6b-a1c2-3d4e5f6a7b8c/slots",
    headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
    json={
        "timezone": "America/Lima",
        "slots": {
            "linkedin": {
                "mon": ["08:30", "12:00"],
                "tue": ["08:30", "12:00"],
                "wed": ["08:30"],
                "thu": ["08:30"],
                "fri": ["08:30"],
                "sat": [],
                "sun": [],
            },
            "tiktok": None,
        },
    },
)
print(r.status_code, r.json())
```

`tiktok: null` puts TikTok back on the default slots. Networks you do not name keep theirs.

## List queued posts

`GET /api/schedule`

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

**Parameters**

| Field | Type | Description |
|---|---|---|
| `from` | query, string (date-time) | Only posts due at or after this time. |
| `to` | query, string (date-time) | Only posts due at or before this time. |

**Responses**

| Status | Means |
|---|---|
| `200` | { scheduled: [...] } |

```bash
curl "https://postwire.io/api/schedule?from=2026-10-01T00%3A00%3A00Z&to=2026-10-31T23%3A59%3A59Z" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY"
```

```js
const res = await fetch("https://postwire.io/api/schedule?from=2026-10-01T00%3A00%3A00Z&to=2026-10-31T23%3A59%3A59Z", {
  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/schedule?from=2026-10-01T00%3A00%3A00Z&to=2026-10-31T23%3A59%3A59Z",
    headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
)
print(r.status_code, r.json())
```

## One scheduled post with each network's result

`GET /api/schedule/{id}`

Also follows a large YouTube/TikTok upload that continues after /api/post answered: its result carries this id as schedule_id. `uploading` shows the bytes sent so far.

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

**Parameters**

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

**Responses**

| Status | Means |
|---|---|
| `200` | { scheduled } |
| `404` | not_found |

```bash
curl "https://postwire.io/api/schedule/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY"
```

```js
const res = await fetch("https://postwire.io/api/schedule/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f", {
  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/schedule/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f",
    headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
)
print(r.status_code, r.json())
```

## Move or edit a queued post

`PATCH /api/schedule/{id}`

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

**Parameters**

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

**Body (JSON)**

| Field | Type | Description |
|---|---|---|
| `run_at` | string (date-time) |  |
| `label` | string | Up to 120 characters. |
| `payload` | object | The post itself, same shape as POST /api/post (text, per_platform, media, options). |

**Responses**

| Status | Means |
|---|---|
| `200` | { ok, scheduled } |
| `409` | no longer queued |

```bash
curl -X PATCH "https://postwire.io/api/schedule/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "run_at": "2026-10-06T15:30:00Z"
}'
```

```js
const res = await fetch("https://postwire.io/api/schedule/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f", {
  method: "PATCH",
  headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    "run_at": "2026-10-06T15:30:00Z"
  }),
});
console.log(res.status, await res.json());
```

```python
import os, requests

r = requests.patch(
    "https://postwire.io/api/schedule/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f",
    headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
    json={
        "run_at": "2026-10-06T15:30:00Z",
    },
)
print(r.status_code, r.json())
```

## Cancel a queued or held post

`DELETE /api/schedule/{id}`

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

**Parameters**

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

**Responses**

| Status | Means |
|---|---|
| `200` | { ok } |
| `409` | no longer queued |

```bash
curl -X DELETE "https://postwire.io/api/schedule/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY"
```

```js
const res = await fetch("https://postwire.io/api/schedule/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f", {
  method: "DELETE",
  headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}` },
});
console.log(res.status, await res.json());
```

```python
import os, requests

r = requests.delete(
    "https://postwire.io/api/schedule/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f",
    headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
)
print(r.status_code, r.json())
```

## Write and queue a week of posts from one topic

`POST /api/week`

One topic in, one post a day out: each day is written from a different angle for every network asked, and queued at start_hour in the given IANA timezone, starting tomorrow. Text networks only (a video network answers media_required). Counts as one AI draft against the daily limit; 3 week plans an hour. Days that do not fit the plan (the Free queue of 3, or the month's posts) are written and saved as held, never dropped.

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

**Body (JSON)**

| Field | Type | Description |
|---|---|---|
| `topic` (required) | string | Up to 2000 characters. |
| `platforms` (required) | array of string | One of: tiktok, instagram, youtube, linkedin, facebook, bluesky, mastodon, telegram, discord. |
| `days` | integer | Default 5. 1 to 7. |
| `start_hour` | integer | Hour of the day, on the wall clock of timezone. Default 10. 0 to 23. |
| `timezone` | string | IANA name, e.g. America/Lima. Without it, tz_offset_minutes (getTimezoneOffset convention) or UTC. |
| `tz_offset_minutes` | integer |  |
| `brand_voice` | string | Up to 1500 characters. |
| `brand_id` | string (uuid) |  |

**Responses**

| Status | Means |
|---|---|
| `200` | { ok, queued: [{ id, run_at, status, preview, posts }], held?: [...], failed: [], targets, message, held_networks?, upgrade? } |
| `400` | topic required, platforms[] required, bad_timezone, not_connected, media_required |
| `409` | ambiguous_brand |
| `429` | rate_limited, or the daily AI limit |

```bash
curl -X POST "https://postwire.io/api/week" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "topic": "How we bake sourdough overnight",
  "platforms": [
    "linkedin",
    "bluesky"
  ],
  "days": 5,
  "start_hour": 9,
  "timezone": "America/Lima"
}'
```

```js
const res = await fetch("https://postwire.io/api/week", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    "topic": "How we bake sourdough overnight",
    "platforms": [
      "linkedin",
      "bluesky"
    ],
    "days": 5,
    "start_hour": 9,
    "timezone": "America/Lima"
  }),
});
console.log(res.status, await res.json());
```

```python
import os, requests

r = requests.post(
    "https://postwire.io/api/week",
    headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
    json={
        "topic": "How we bake sourdough overnight",
        "platforms": ["linkedin", "bluesky"],
        "days": 5,
        "start_hour": 9,
        "timezone": "America/Lima",
    },
)
print(r.status_code, r.json())
```
