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.
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: Queue a post for a later timeGET /api/schedule/next-slot: Where run_at "next_slot" would put a post now (nothing is queued)GET /api/brands/{id}/slots: A brand's queue slots: weekly times per network, and the timezonePUT /api/brands/{id}/slots: Set a brand's queue slots and timezoneGET /api/schedule: List queued postsGET /api/schedule/{id}: One scheduled post with each network's resultPATCH /api/schedule/{id}: Move or edit a queued postDELETE /api/schedule/{id}: Cancel a queued or held postPOST /api/week: Write and queue a week of posts from one topic
Queue a post for a later time
/api/scheduleRules 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 |
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"
}'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());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
{
"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)
/api/schedule/next-slotA 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 |
curl "https://postwire.io/api/schedule/next-slot?platforms=linkedin%2Cbluesky&timezone=America%2FLima" \
-H "Authorization: Bearer $POSTWIRE_API_KEY"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());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
{
"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
/api/brands/{id}/slotsEach 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 |
curl "https://postwire.io/api/brands/b3f1c2d4-8a9e-4f6b-a1c2-3d4e5f6a7b8c/slots" \
-H "Authorization: Bearer $POSTWIRE_API_KEY"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());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
/api/brands/{id}/slotsReplaces 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) |
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
}
}'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());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
/api/scheduleNeeds 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: [...] } |
curl "https://postwire.io/api/schedule?from=2026-10-01T00%3A00%3A00Z&to=2026-10-31T23%3A59%3A59Z" \
-H "Authorization: Bearer $POSTWIRE_API_KEY"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());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
/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 |
curl "https://postwire.io/api/schedule/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f" \
-H "Authorization: Bearer $POSTWIRE_API_KEY"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());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
/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 |
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"
}'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());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
/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 |
curl -X DELETE "https://postwire.io/api/schedule/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f" \
-H "Authorization: Bearer $POSTWIRE_API_KEY"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());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
/api/weekOne 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 |
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"
}'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());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())