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.

View as MarkdownOpenAPIBase URL https://postwire.io

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.

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)

FieldTypeDescription
platforms requiredarray of stringOne of: tiktok, instagram, youtube, linkedin, facebook, bluesky, mastodon, telegram, discord.
textstring
run_at requiredstringWhen 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_urlstring (uri)
brand_idstring (uuid)
labelstringUp to 120 characters.
timezonestringWith 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

StatusMeans
200{ ok, scheduled: {...}, targets[], message, held_networks?, held_message?, upgrade? }
202Saved 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 }
400run_at in the past, network not connected, media required, or the 500-post sanity ceiling
409code no_free_slot (run_at next_slot found no free slot in 28 days), or ambiguous_brand
curl
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"
}'
Node
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

FieldTypeDescription
platforms requiredquery, stringComma-separated networks, e.g. linkedin,bluesky.
brand_idquery, string (uuid)
timezonequery, stringIANA timezone, used when the brand has none saved.

Responses

StatusMeans
200{ brand_id, platforms, run_at, timezone, local, shared }
400platforms missing or unknown, bad_timezone
409no_free_slot
curl
curl "https://postwire.io/api/schedule/next-slot?platforms=linkedin%2Cbluesky&timezone=America%2FLima" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY"
Node
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

FieldTypeDescription
id requiredpath, string (uuid)
timezonequery, stringShown when the brand has none saved.

Responses

StatusMeans
200{ brand_id, name, timezone, timezone_saved, slots: { <network>: { mon: ["09:00"], … } }, custom: [], defaults, rule }
404brand_not_found
curl
curl "https://postwire.io/api/brands/b3f1c2d4-8a9e-4f6b-a1c2-3d4e5f6a7b8c/slots" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY"
Node
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

FieldTypeDescription
id requiredpath, string (uuid)

Body (JSON)

FieldTypeDescription
timezonestringIANA name, e.g. America/Lima. null clears it.
slotsobjectKeyed by network.
slots.tiktokobjectDays to times, 24-hour HH:MM in the brand's timezone.
slots.instagramobjectDays to times, 24-hour HH:MM in the brand's timezone.
slots.youtubeobjectDays to times, 24-hour HH:MM in the brand's timezone.
slots.linkedinobjectDays to times, 24-hour HH:MM in the brand's timezone.
slots.facebookobjectDays to times, 24-hour HH:MM in the brand's timezone.
slots.blueskyobjectDays to times, 24-hour HH:MM in the brand's timezone.
slots.mastodonobjectDays to times, 24-hour HH:MM in the brand's timezone.
slots.telegramobjectDays to times, 24-hour HH:MM in the brand's timezone.
slots.discordobjectDays to times, 24-hour HH:MM in the brand's timezone.

Responses

StatusMeans
200{ ok, …the same body as GET }
400bad_timezone, or a bad day, time or network (bad_request)
404brand_not_found
503slots_unavailable: slots cannot be saved on this server yet (the defaults still apply)
curl
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
  }
}'
Node
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

FieldTypeDescription
fromquery, string (date-time)Only posts due at or after this time.
toquery, string (date-time)Only posts due at or before this time.

Responses

StatusMeans
200{ scheduled: [...] }
curl
curl "https://postwire.io/api/schedule?from=2026-10-01T00%3A00%3A00Z&to=2026-10-31T23%3A59%3A59Z" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY"
Node
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

FieldTypeDescription
id requiredpath, string

Responses

StatusMeans
200{ scheduled }
404not_found
curl
curl "https://postwire.io/api/schedule/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY"
Node
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

FieldTypeDescription
id requiredpath, string

Body (JSON)

FieldTypeDescription
run_atstring (date-time)
labelstringUp to 120 characters.
payloadobjectThe post itself, same shape as POST /api/post (text, per_platform, media, options).

Responses

StatusMeans
200{ ok, scheduled }
409no longer queued
curl
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"
}'
Node
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

FieldTypeDescription
id requiredpath, string

Responses

StatusMeans
200{ ok }
409no longer queued
curl
curl -X DELETE "https://postwire.io/api/schedule/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY"
Node
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)

FieldTypeDescription
topic requiredstringUp to 2000 characters.
platforms requiredarray of stringOne of: tiktok, instagram, youtube, linkedin, facebook, bluesky, mastodon, telegram, discord.
daysintegerDefault 5. 1 to 7.
start_hourintegerHour of the day, on the wall clock of timezone. Default 10. 0 to 23.
timezonestringIANA name, e.g. America/Lima. Without it, tz_offset_minutes (getTimezoneOffset convention) or UTC.
tz_offset_minutesinteger
brand_voicestringUp to 1500 characters.
brand_idstring (uuid)

Responses

StatusMeans
200{ ok, queued: [{ id, run_at, status, preview, posts }], held?: [...], failed: [], targets, message, held_networks?, upgrade? }
400topic required, platforms[] required, bad_timezone, not_connected, media_required
409ambiguous_brand
429rate_limited, or the daily AI limit
curl
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"
}'
Node
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())