Bulk upload, recurring posts and RSS
A month of posts from one CSV, checked row by row and scheduled whole or not at all; posts that repeat; RSS feeds that become posts.
Bulk upload. Send a CSV (or rows as JSON) to POST /api/bulk. It is a dry run by default: every row comes back with its time in its timezone, each network's length as that network counts it (on X a link counts 23 and an emoji 2), the X credits it will use, whether it waits for approval or is held by the plan, and its errors and warnings — with the line number. Send it again with dry_run: false and all rows are scheduled, or none: one row with an error answers 422 bulk_rows_invalid and nothing is stored. The same file twice answers 409 bulk_already_scheduled (a double click never doubles your posts), and DELETE /api/bulk/{batch_id} undoes a whole upload. Columns: date (2026-11-02 09:00 in the row's or the upload's timezone, an ISO time with its offset, or next_slot), timezone, networks, text, text_<network>, media_urls, media_urls_<network>, alt_text, first_comment (LinkedIn), x_reply, x_thread, title, brand, label, privacy. Comma, semicolon or tab; UTF-8 with or without a BOM. Download the template. Rows per upload: Free 20, Starter 500, Pro 1,000, Agency 2,000, Scale 5,000; on Free, posts past the 3 scheduled at a time are saved as held.
Recurring posts (every paid plan). Daily, weekly on some weekdays, or monthly on some days (31 falls on the last day of shorter months; -1 is always the last day), every N days, weeks or months, at 1-4 times a day in a timezone, until a date or for a number of times. Daylight saving is handled the way calendars do it. variants rotate in order. Occurrences are put in the queue up to 7 days ahead as ordinary scheduled posts: each can be edited or canceled alone, and editing the recurring post replaces every waiting occurrence except the ones edited by hand. The duplicate guard (guard_days, default 3) skips a network where the same text went out in the last N days when an occurrence is published (code duplicate_recent): networks flag repeated posts, and X refuses them.
RSS feeds. RSS 2.0, RSS 1.0 and Atom, read as often as every 30 minutes. What is already in the feed when you add it is recorded, not posted. Each new item — new by its guid and its link — becomes a post from your template ({title}, {link}, {summary}), fitted to each network's limit without ever cutting the link; or written per network with AI, from the plan's AI drafts a day, never an extra charge. New items go out right away or in the brand's next free slot, or wait for approval. Feeds are fetched safely: private, loopback and cloud-metadata addresses are refused, also after a redirect or when a name resolves to one; 3 redirects, 8 seconds and 2 MB at most.
Everything here goes through the brand's approval rules, the key's limits, X credits and the plan's monthly posts, like any post, and is written to the activity log.
POST /api/bulk: Bulk upload: check, then schedule many posts at once (CSV or rows)GET /api/bulk/template.csv: The bulk upload templateDELETE /api/bulk/{id}: Undo a bulk uploadPOST /api/recurring: Set up a recurring postPOST /api/recurring/preview: Preview a ruleGET /api/recurring: List recurring postsGET /api/recurring/{id}: A recurring postPATCH /api/recurring/{id}: Edit all occurrences, pause or resumeDELETE /api/recurring/{id}: Delete a recurring postPOST /api/rss/preview: Preview a feedPOST /api/rss: Add an RSS feedGET /api/rss: List RSS feedsPATCH /api/rss/{id}: Change, pause or turn on a feedDELETE /api/rss/{id}: Remove a feedPOST /api/rss/{id}/check: Read a feed nowGET /api/rss/{id}/items: What happened to a feed's items
Bulk upload: check, then schedule many posts at once (CSV or rows)
/api/bulkA CSV (field csv, or the request body itself with Content-Type: text/csv) or a list of rows. Columns: date ("2026-11-02 09:00" in the row's timezone or the upload's, an ISO time with its offset, or next_slot), time (optional), timezone, networks, text, text_<network> (each network's own text), title, title_<network>, media_urls (https; prefix video: or document:), media_urls_<network>, image_urls, video_url, alt_text (| between images), first_comment (LinkedIn), x_reply, x_thread (||| between posts), brand (name or id), label, privacy. Comma, semicolon or tab separated; UTF-8 with or without a BOM; quoted fields may hold commas and line breaks. dry_run is true by default: nothing is stored, and every row comes back with its time in its timezone, its networks, each network's length as that network counts it (X: a link 23, an emoji 2), the X credits it would use, whether it waits for approval (the brand's rules, a key's limits, a Contributor) or is held by the plan, and its errors and warnings. With dry_run false, ALL rows are scheduled or NONE: any row with an error answers 422 bulk_rows_invalid and nothing is stored; the rows that go to the queue are inserted in one statement; if the rows that wait for approval cannot be stored, the inserted ones are canceled again (503). The same upload twice answers 409 bulk_already_scheduled unless allow_duplicate is true. Rows per upload: Free 20, Starter 500, Pro 1,000, Agency 2,000, Scale 5,000 (403 bulk_rows_limit past it). On Free, rows past the 3 scheduled posts at a time are saved as held (never published on Free).
Needs an API key: Authorization: Bearer pw_live_….
Body (JSON)
| Field | Type | Description |
|---|---|---|
csv | string | The CSV text, first line = column names. Up to 3 MB. |
rows | array of object | Instead of csv: objects with the same column names. |
dry_run | boolean | Default true. |
timezone | string | IANA timezone for dates without one. |
brand_id | string | Default brand (id or name) for rows without a brand column. |
fit_long_text | boolean | A text over a network's limit is cut at a sentence or word when published (a warning), instead of an error. Default false. |
date_format | string | How to read 12/10/2026. One of: ymd, dmy, mdy. Default "ymd". |
allow_duplicate | boolean | Default false. |
check_media | boolean | Check that each media link answers (each link once, up to 20 seconds in all). Default true. |
batch_id | string | Your own id for the upload (6-64 letters, digits, - or _). Default: a hash of the file. |
Responses
| Status | Means |
|---|---|
200 | { ok, dry_run, batch_id, summary: { rows, ok, with_errors, with_warnings, posts, x_credits, wait_for_approval, held, will_queue, plan }, rows: [{ line, ok, run_at, local, timezone, networks, brand, preview, lengths, x_credits?, approval?, held_for?, errors: [{ field, network?, message, code }], warnings, schedule_id?, status? }], x_credits?, ignored_columns?, undo?, message } |
400 | bad_csv (with line), bad_timezone, bad_request |
403 | bulk_rows_limit (with the plan that takes the upload), email not verified |
409 | bulk_already_scheduled |
422 | bulk_rows_invalid: nothing was scheduled; each row says what to fix |
503 | bulk_store_failed / governance_unavailable: nothing was scheduled |
curl -X POST "https://postwire.io/api/bulk" \
-H "Authorization: Bearer $POSTWIRE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"csv": "date,networks,text,first_comment\n2026-11-02 09:00,\"linkedin, bluesky\",Our autumn menu is here,Full menu: https://bakery.example/menu\n2026-11-03 12:30,instagram,Fresh out of the oven",
"timezone": "America/Lima",
"dry_run": true
}'const res = await fetch("https://postwire.io/api/bulk", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
"csv": "date,networks,text,first_comment\n2026-11-02 09:00,\"linkedin, bluesky\",Our autumn menu is here,Full menu: https://bakery.example/menu\n2026-11-03 12:30,instagram,Fresh out of the oven",
"timezone": "America/Lima",
"dry_run": true
}),
});
console.log(res.status, await res.json());import os, requests
r = requests.post(
"https://postwire.io/api/bulk",
headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
json={
"csv": "date,networks,text,first_comment\n2026-11-02 09:00,\"linkedin, bluesky\",Our autumn menu is here,Full menu: https://bakery.example/menu\n2026-11-03 12:30,instagram,Fresh out of the oven",
"timezone": "America/Lima",
"dry_run": True,
},
)
print(r.status_code, r.json())Example response
{
"ok": false,
"dry_run": true,
"batch_id": "5681dfcaa8a49e2bd2a6",
"summary": {
"rows": 2,
"ok": 1,
"with_errors": 1,
"posts": 2,
"x_credits": 0,
"wait_for_approval": 0,
"held": 0,
"will_queue": 1
},
"rows": [
{
"line": 2,
"ok": true,
"run_at": "2026-11-02T14:00:00.000Z",
"local": "2026-11-02 09:00",
"timezone": "America/Lima",
"networks": [
"linkedin",
"bluesky"
],
"lengths": {
"linkedin": 23,
"bluesky": 23
},
"errors": [],
"warnings": []
},
{
"line": 3,
"ok": false,
"networks": [
"instagram"
],
"errors": [
{
"field": "media",
"network": "instagram",
"message": "instagram needs a photo or a video",
"code": "media_required"
}
],
"warnings": []
}
],
"message": "1 of 2 rows have problems: fix them and check again. Nothing is scheduled until every row is right."
}Send the same body with dry_run: false to schedule every row, or none. Or send the file itself: curl --data-binary @posts.csv -H "Content-Type: text/csv" "https://postwire.io/api/bulk?timezone=America/Lima".
The bulk upload template
/api/bulk/template.csvA CSV with every column and three example rows (dates a week from today). No key needed.
No API key needed.
Responses
| Status | Means |
|---|---|
200 | text/csv |
curl "https://postwire.io/api/bulk/template.csv"const res = await fetch("https://postwire.io/api/bulk/template.csv", {
method: "GET",
});
console.log(res.status, await res.json());import os, requests
r = requests.get(
"https://postwire.io/api/bulk/template.csv",
)
print(r.status_code, r.json())Undo a bulk upload
/api/bulk/{id}Cancels every post of the upload (its batch_id) that has not gone out, and withdraws the ones waiting for approval. Posts already published stay.
Needs an API key: Authorization: Bearer pw_live_….
Parameters
| Field | Type | Description |
|---|---|---|
id required | path, string |
Responses
| Status | Means |
|---|---|
200 | { ok, batch_id, canceled, already_out, message } |
404 | not_found |
curl -X DELETE "https://postwire.io/api/bulk/5681dfcaa8a49e2bd2a6" \
-H "Authorization: Bearer $POSTWIRE_API_KEY"const res = await fetch("https://postwire.io/api/bulk/5681dfcaa8a49e2bd2a6", {
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/bulk/5681dfcaa8a49e2bd2a6",
headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
)
print(r.status_code, r.json())Example response
{
"ok": true,
"batch_id": "5681dfcaa8a49e2bd2a6",
"canceled": 12,
"already_out": 0
}Set up a recurring post
/api/recurringEvery paid plan (Starter 25, Pro 100, Agency 500, Scale 2,000 at a time; 403 plan_feature on Free). Occurrences are put in the queue up to 7 days ahead (at most 14 at once) as ordinary scheduled posts with payload._recurring: each can be edited (PATCH /api/schedule/{id}) or canceled alone, and goes through the brand's approval rules, the key's limits, X credits and the plan's monthly posts like any post. An occurrence whose time passed while the publisher was not running is never posted late. Answers with warnings when the duplicate guard would skip posts (one text every day with a 3-day guard).
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, x. |
text | string | The post, when there is one text. |
variants | array of any | Texts used in turn: occurrence n uses variant ((n-1) mod count) + 1. Up to 50 items. |
rule required | object | When it repeats. Daylight saving: a time that does not exist on the day clocks go forward runs that much later (02:30 -> 03:30); a time that happens twice runs the first time. |
rule.freq required | string | One of: daily, weekly, monthly. |
rule.interval | integer | Every N days / weeks / months, counted from start. Default 1. 1 to 52. |
rule.days | array of string | weekly: the weekdays (default: the start date's). One of: mon, tue, wed, thu, fri, sat, sun. |
rule.month_days | array of integer | monthly: days of the month; -1 = the last day. 31 falls on the last day of shorter months. |
rule.times | array of string | 24-hour HH:MM, 1-4 a day. Default 09:00. Up to 4 items. |
rule.timezone | string | IANA name. Default: the brand's queue timezone, else UTC. |
rule.start | string (date) | First day (default today); also the anchor of every N. |
rule.until | string (date) | Last day, inclusive. |
rule.count | integer | Stop after N occurrences. 1 to 1000. |
guard_days | integer | When an occurrence is published, a network where the same text went out in the last N days (3-hour margin) is skipped, with code duplicate_recent in its result. 0 = off. Default 3. 0 to 90. |
per_platform | object | |
media | array of object | |
photo_url | string | |
video_url | string | |
options | object | |
title | string | |
label | string | Up to 120 characters. |
brand_id | string (uuid) |
Responses
| Status | Means |
|---|---|
201 | { ok, recurring, queued_now, warnings, message } |
400 | bad_rule, not_connected, text_required, too_long, media_required |
403 | plan_feature, recurring_limit, x_paid_plan_required |
503 | automation_unavailable |
curl -X POST "https://postwire.io/api/recurring" \
-H "Authorization: Bearer $POSTWIRE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"platforms": [
"linkedin",
"bluesky"
],
"variants": [
"Monday tip: grind right before you brew.",
"Monday tip: weigh your coffee, 1:16 is a good start.",
"Monday tip: water just off the boil."
],
"rule": {
"freq": "weekly",
"days": [
"mon"
],
"times": [
"09:00"
],
"timezone": "America/Lima"
},
"guard_days": 7,
"label": "Monday tip"
}'const res = await fetch("https://postwire.io/api/recurring", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
"platforms": [
"linkedin",
"bluesky"
],
"variants": [
"Monday tip: grind right before you brew.",
"Monday tip: weigh your coffee, 1:16 is a good start.",
"Monday tip: water just off the boil."
],
"rule": {
"freq": "weekly",
"days": [
"mon"
],
"times": [
"09:00"
],
"timezone": "America/Lima"
},
"guard_days": 7,
"label": "Monday tip"
}),
});
console.log(res.status, await res.json());import os, requests
r = requests.post(
"https://postwire.io/api/recurring",
headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
json={
"platforms": ["linkedin", "bluesky"],
"variants": [
"Monday tip: grind right before you brew.",
"Monday tip: weigh your coffee, 1:16 is a good start.",
"Monday tip: water just off the boil.",
],
"rule": {
"freq": "weekly",
"days": ["mon"],
"times": ["09:00"],
"timezone": "America/Lima",
},
"guard_days": 7,
"label": "Monday tip",
},
)
print(r.status_code, r.json())Example response
{
"ok": true,
"recurring": {
"id": "7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f",
"status": "active",
"describe": "Every week on Mon at 09:00 (America/Lima), from 2026-10-05",
"next": [
{
"at": "2026-10-05T14:00:00.000Z",
"local": "2026-10-05 09:00",
"n": 1,
"variant": 1,
"schedule_id": "c1a…",
"status": "queued"
}
]
},
"queued_now": 1,
"warnings": []
}Preview a rule
/api/recurring/previewThe next occurrences of a rule (and which text each would use) and the warnings, without saving anything.
Needs an API key: Authorization: Bearer pw_live_….
Body (JSON)
| Field | Type | Description |
|---|---|---|
rule | object | When it repeats. Daylight saving: a time that does not exist on the day clocks go forward runs that much later (02:30 -> 03:30); a time that happens twice runs the first time. |
rule.freq required | string | One of: daily, weekly, monthly. |
rule.interval | integer | Every N days / weeks / months, counted from start. Default 1. 1 to 52. |
rule.days | array of string | weekly: the weekdays (default: the start date's). One of: mon, tue, wed, thu, fri, sat, sun. |
rule.month_days | array of integer | monthly: days of the month; -1 = the last day. 31 falls on the last day of shorter months. |
rule.times | array of string | 24-hour HH:MM, 1-4 a day. Default 09:00. Up to 4 items. |
rule.timezone | string | IANA name. Default: the brand's queue timezone, else UTC. |
rule.start | string (date) | First day (default today); also the anchor of every N. |
rule.until | string (date) | Last day, inclusive. |
rule.count | integer | Stop after N occurrences. 1 to 1000. |
variants | array of string | |
guard_days | integer | |
brand_id | string | |
limit | integer |
Responses
| Status | Means |
|---|---|
200 | { rule, describe, next: [{ at, local, n, variant }], warnings } |
400 | bad_rule |
curl -X POST "https://postwire.io/api/recurring/preview" \
-H "Authorization: Bearer $POSTWIRE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"rule": {
"freq": "monthly",
"month_days": [
-1
],
"times": [
"10:00"
],
"timezone": "Europe/Madrid"
},
"variants": [
"Order by tonight for next month'\''s box."
]
}'const res = await fetch("https://postwire.io/api/recurring/preview", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
"rule": {
"freq": "monthly",
"month_days": [
-1
],
"times": [
"10:00"
],
"timezone": "Europe/Madrid"
},
"variants": [
"Order by tonight for next month's box."
]
}),
});
console.log(res.status, await res.json());import os, requests
r = requests.post(
"https://postwire.io/api/recurring/preview",
headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
json={
"rule": {
"freq": "monthly",
"month_days": [-1],
"times": ["10:00"],
"timezone": "Europe/Madrid",
},
"variants": ["Order by tonight for next month's box."],
},
)
print(r.status_code, r.json())Example response
{
"describe": "Every month on the last day at 10:00 (Europe/Madrid), from 2026-10-05",
"next": [
{
"local": "2026-10-31 10:00"
},
{
"local": "2026-11-30 10:00"
}
],
"warnings": []
}List recurring posts
/api/recurringEach with its rule in words (describe), its status (active, paused, ended), and its next occurrences: the ones in the queue with their schedule_id and status, then the later ones (not_yet_queued).
Needs an API key: Authorization: Bearer pw_live_….
Responses
| Status | Means |
|---|---|
200 | { recurring: [{ id, label, platforms, variants, rule, describe, guard_days, status, paused_reason, occurrences, next_at, last_error, next: [...] }], plan: { name, limit, used }, setup_pending? } |
curl "https://postwire.io/api/recurring" \
-H "Authorization: Bearer $POSTWIRE_API_KEY"const res = await fetch("https://postwire.io/api/recurring", {
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/recurring",
headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
)
print(r.status_code, r.json())A recurring post
/api/recurring/{id}Needs an API key: Authorization: Bearer pw_live_….
Parameters
| Field | Type | Description |
|---|---|---|
id required | path, string (uuid) |
Responses
| Status | Means |
|---|---|
200 | { recurring (with next), history: [{ schedule_id, at, n, status, error }], warnings } |
404 | not_found |
curl "https://postwire.io/api/recurring/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f" \
-H "Authorization: Bearer $POSTWIRE_API_KEY"const res = await fetch("https://postwire.io/api/recurring/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/recurring/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f",
headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
)
print(r.status_code, r.json())Edit all occurrences, pause or resume
/api/recurring/{id}Changing the content, the rule, the networks or the guard replaces every waiting occurrence that was not edited by hand; the edited ones stay. status paused cancels the waiting occurrences (not the edited ones); active puts them back.
Needs an API key: Authorization: Bearer pw_live_….
Parameters
| Field | Type | Description |
|---|---|---|
id required | path, string (uuid) |
Body (JSON)
| Field | Type | Description |
|---|---|---|
platforms | array of string | One of: tiktok, instagram, youtube, linkedin, facebook, bluesky, mastodon, telegram, discord, x. |
text | string | The post, when there is one text. |
variants | array of any | Texts used in turn: occurrence n uses variant ((n-1) mod count) + 1. Up to 50 items. |
rule | object | When it repeats. Daylight saving: a time that does not exist on the day clocks go forward runs that much later (02:30 -> 03:30); a time that happens twice runs the first time. |
rule.freq required | string | One of: daily, weekly, monthly. |
rule.interval | integer | Every N days / weeks / months, counted from start. Default 1. 1 to 52. |
rule.days | array of string | weekly: the weekdays (default: the start date's). One of: mon, tue, wed, thu, fri, sat, sun. |
rule.month_days | array of integer | monthly: days of the month; -1 = the last day. 31 falls on the last day of shorter months. |
rule.times | array of string | 24-hour HH:MM, 1-4 a day. Default 09:00. Up to 4 items. |
rule.timezone | string | IANA name. Default: the brand's queue timezone, else UTC. |
rule.start | string (date) | First day (default today); also the anchor of every N. |
rule.until | string (date) | Last day, inclusive. |
rule.count | integer | Stop after N occurrences. 1 to 1000. |
guard_days | integer | When an occurrence is published, a network where the same text went out in the last N days (3-hour margin) is skipped, with code duplicate_recent in its result. 0 = off. Default 3. 0 to 90. |
per_platform | object | |
media | array of object | |
photo_url | string | |
video_url | string | |
options | object | |
title | string | |
label | string | Up to 120 characters. |
brand_id | string (uuid) | |
status | string | One of: active, paused. |
Responses
| Status | Means |
|---|---|
200 | { ok, recurring, replaced, queued_now, warnings } |
404 | not_found |
curl -X PATCH "https://postwire.io/api/recurring/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f" \
-H "Authorization: Bearer $POSTWIRE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"status": "paused"
}'const res = await fetch("https://postwire.io/api/recurring/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f", {
method: "PATCH",
headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
"status": "paused"
}),
});
console.log(res.status, await res.json());import os, requests
r = requests.patch(
"https://postwire.io/api/recurring/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f",
headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
json={
"status": "paused",
},
)
print(r.status_code, r.json())Delete a recurring post
/api/recurring/{id}Its waiting occurrences are canceled (and ones waiting for approval withdrawn), unless ?keep_queued=1.
Needs an API key: Authorization: Bearer pw_live_….
Parameters
| Field | Type | Description |
|---|---|---|
id required | path, string (uuid) | |
keep_queued | query, string |
Responses
| Status | Means |
|---|---|
200 | { ok, canceled } |
404 | not_found |
curl -X DELETE "https://postwire.io/api/recurring/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f" \
-H "Authorization: Bearer $POSTWIRE_API_KEY"const res = await fetch("https://postwire.io/api/recurring/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/recurring/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f",
headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
)
print(r.status_code, r.json())Preview a feed
/api/rss/previewReads the feed (with the same protections) and returns its title, its newest items and what each network's post of the newest would read. Nothing is saved. 30 an hour.
Needs an API key: Authorization: Bearer pw_live_….
Body (JSON)
| Field | Type | Description |
|---|---|---|
url required | string | RSS 2.0, RSS 1.0 or Atom. http or https on ports 80, 443, 8080 or 8443; private, loopback, link-local and metadata addresses are refused (400 feed_url_blocked), also when a name resolves to one or a redirect points at one; at most 3 redirects, 8 seconds, 2 MB. |
platforms | array of string | One of: tiktok, instagram, youtube, linkedin, facebook, bluesky, mastodon, telegram, discord, x. |
template | string | {title}, {link}, {summary}. Fitted to each network's limit as that network counts it: the summary is shortened first, then the title; the link is never cut. Default "{title}\n\n{link}". Up to 2000 characters. |
Responses
| Status | Means |
|---|---|
200 | { ok, url, kind, title, items_in_feed, items, posts: { <network>: { text, trimmed? } }, note } |
400 | feed_url_blocked, bad_feed_url |
422 | not_a_feed, feed_http_error, feed_timeout, feed_too_large, feed_too_many_redirects |
curl -X POST "https://postwire.io/api/rss/preview" \
-H "Authorization: Bearer $POSTWIRE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://bakery.example/feed.xml",
"platforms": [
"linkedin",
"x"
],
"template": "{title}\n\n{link}"
}'const res = await fetch("https://postwire.io/api/rss/preview", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
"url": "https://bakery.example/feed.xml",
"platforms": [
"linkedin",
"x"
],
"template": "{title}\n\n{link}"
}),
});
console.log(res.status, await res.json());import os, requests
r = requests.post(
"https://postwire.io/api/rss/preview",
headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
json={
"url": "https://bakery.example/feed.xml",
"platforms": ["linkedin", "x"],
"template": "{title}\n\n{link}",
},
)
print(r.status_code, r.json())Add an RSS feed
/api/rssFeeds per account: Free 3, Starter 25, Pro 100, Agency 500, Scale 2,000. The first read happens now and only records what is already in the feed. Each new item (new by its guid AND its link) becomes a post, queued now or in the brand's next free slot, or sent to approvals; brand approval rules for automations apply too. A feed is turned off after 10 failed reads in a row.
Needs an API key: Authorization: Bearer pw_live_….
Body (JSON)
| Field | Type | Description |
|---|---|---|
url required | string | RSS 2.0, RSS 1.0 or Atom. http or https on ports 80, 443, 8080 or 8443; private, loopback, link-local and metadata addresses are refused (400 feed_url_blocked), also when a name resolves to one or a redirect points at one; at most 3 redirects, 8 seconds, 2 MB. |
platforms required | array of string | Not YouTube or TikTok (video only). Instagram gets an item only when it has an image. One of: tiktok, instagram, youtube, linkedin, facebook, bluesky, mastodon, telegram, discord, x. |
template | string | {title}, {link}, {summary}. Fitted to each network's limit as that network counts it: the summary is shortened first, then the title; the link is never cut. Default "{title}\n\n{link}". Up to 2000 characters. |
interval_minutes | integer | Default 60. 30 to 10080. |
max_items | integer | New items posted per read, oldest first; the rest wait for the next read. Default 3. 1 to 10. |
mode | string | approval: every new item waits for approval (paid plans). One of: auto, approval. Default "auto". |
post_at | string | One of: now, next_slot. Default "now". |
include_image | boolean | Default true. |
ai_rewrite | boolean | Write each network's post with AI: one of the plan's AI drafts a day per item, never an extra charge; the template when they are used up. Default false. |
post_latest | boolean | Also post the newest item now. Otherwise the items already in the feed are recorded, not posted. Default false. |
enabled | boolean | |
brand_id | string (uuid) |
Responses
| Status | Means |
|---|---|
201 | { ok, feed, first_read: { primed? | posted? | error? }, message } |
400 | feed_url_blocked, bad_feed_url, bad_template, media_required, not_connected |
403 | plan_feature, rss_feed_limit |
409 | rss_feed_exists |
503 | automation_unavailable |
curl -X POST "https://postwire.io/api/rss" \
-H "Authorization: Bearer $POSTWIRE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://bakery.example/feed.xml",
"platforms": [
"linkedin",
"bluesky"
],
"template": "{title}\n\n{summary}\n\n{link}",
"interval_minutes": 60,
"max_items": 3,
"mode": "auto",
"post_at": "next_slot"
}'const res = await fetch("https://postwire.io/api/rss", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
"url": "https://bakery.example/feed.xml",
"platforms": [
"linkedin",
"bluesky"
],
"template": "{title}\n\n{summary}\n\n{link}",
"interval_minutes": 60,
"max_items": 3,
"mode": "auto",
"post_at": "next_slot"
}),
});
console.log(res.status, await res.json());import os, requests
r = requests.post(
"https://postwire.io/api/rss",
headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
json={
"url": "https://bakery.example/feed.xml",
"platforms": ["linkedin", "bluesky"],
"template": "{title}\n\n{summary}\n\n{link}",
"interval_minutes": 60,
"max_items": 3,
"mode": "auto",
"post_at": "next_slot",
},
)
print(r.status_code, r.json())Example response
{
"ok": true,
"feed": {
"id": "7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f",
"url": "https://bakery.example/feed.xml",
"title": "Bakery journal",
"interval_minutes": 60
},
"first_read": {
"primed": 12
},
"message": "Saved. 12 items already in the feed were recorded and not posted. New items are posted as the feed publishes them (read every 60 minutes)."
}List RSS feeds
/api/rssNeeds an API key: Authorization: Bearer pw_live_….
Responses
| Status | Means |
|---|---|
200 | { feeds: [{ id, url, title, platforms, template, mode, post_at, interval_minutes, max_items, enabled, last_checked_at, last_status, last_error, failures, items_posted, next_check_at }], plan: { limit, used, ai_drafts_a_day, min_interval_minutes, approvals } } |
curl "https://postwire.io/api/rss" \
-H "Authorization: Bearer $POSTWIRE_API_KEY"const res = await fetch("https://postwire.io/api/rss", {
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/rss",
headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
)
print(r.status_code, r.json())Change, pause or turn on a feed
/api/rss/{id}Needs an API key: Authorization: Bearer pw_live_….
Parameters
| Field | Type | Description |
|---|---|---|
id required | path, string (uuid) |
Body (JSON)
| Field | Type | Description |
|---|---|---|
url | string | RSS 2.0, RSS 1.0 or Atom. http or https on ports 80, 443, 8080 or 8443; private, loopback, link-local and metadata addresses are refused (400 feed_url_blocked), also when a name resolves to one or a redirect points at one; at most 3 redirects, 8 seconds, 2 MB. |
platforms | array of string | Not YouTube or TikTok (video only). Instagram gets an item only when it has an image. One of: tiktok, instagram, youtube, linkedin, facebook, bluesky, mastodon, telegram, discord, x. |
template | string | {title}, {link}, {summary}. Fitted to each network's limit as that network counts it: the summary is shortened first, then the title; the link is never cut. Default "{title}\n\n{link}". Up to 2000 characters. |
interval_minutes | integer | Default 60. 30 to 10080. |
max_items | integer | New items posted per read, oldest first; the rest wait for the next read. Default 3. 1 to 10. |
mode | string | approval: every new item waits for approval (paid plans). One of: auto, approval. Default "auto". |
post_at | string | One of: now, next_slot. Default "now". |
include_image | boolean | Default true. |
ai_rewrite | boolean | Write each network's post with AI: one of the plan's AI drafts a day per item, never an extra charge; the template when they are used up. Default false. |
post_latest | boolean | Also post the newest item now. Otherwise the items already in the feed are recorded, not posted. Default false. |
enabled | boolean | |
brand_id | string (uuid) |
Responses
| Status | Means |
|---|---|
200 | { ok, feed } |
404 | not_found |
curl -X PATCH "https://postwire.io/api/rss/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f" \
-H "Authorization: Bearer $POSTWIRE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"enabled": false
}'const res = await fetch("https://postwire.io/api/rss/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f", {
method: "PATCH",
headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
"enabled": false
}),
});
console.log(res.status, await res.json());import os, requests
r = requests.patch(
"https://postwire.io/api/rss/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f",
headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
json={
"enabled": False,
},
)
print(r.status_code, r.json())Remove a feed
/api/rss/{id}Posts it already queued stay in the queue.
Needs an API key: Authorization: Bearer pw_live_….
Parameters
| Field | Type | Description |
|---|---|---|
id required | path, string (uuid) |
Responses
| Status | Means |
|---|---|
200 | { ok } |
404 | not_found |
curl -X DELETE "https://postwire.io/api/rss/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f" \
-H "Authorization: Bearer $POSTWIRE_API_KEY"const res = await fetch("https://postwire.io/api/rss/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/rss/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f",
headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
)
print(r.status_code, r.json())Read a feed now
/api/rss/{id}/checkNeeds an API key: Authorization: Bearer pw_live_….
Parameters
| Field | Type | Description |
|---|---|---|
id required | path, string (uuid) |
Responses
| Status | Means |
|---|---|
200 | { ok, result: { new, posted, results } | { not_modified } | { error }, feed } |
409 | feed_busy |
429 | 6 an hour per feed |
curl -X POST "https://postwire.io/api/rss/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f/check" \
-H "Authorization: Bearer $POSTWIRE_API_KEY"const res = await fetch("https://postwire.io/api/rss/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f/check", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}` },
});
console.log(res.status, await res.json());import os, requests
r = requests.post(
"https://postwire.io/api/rss/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f/check",
headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
)
print(r.status_code, r.json())What happened to a feed's items
/api/rss/{id}/itemsNeeds an API key: Authorization: Bearer pw_live_….
Parameters
| Field | Type | Description |
|---|---|---|
id required | path, string (uuid) | |
limit | query, integer |
Responses
| Status | Means |
|---|---|
200 | { feed, items: [{ item_id, title, link, published_at, status: scheduled | pending_approval | skipped | failed, schedule_id, note, seen_at }] } |
curl "https://postwire.io/api/rss/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f/items" \
-H "Authorization: Bearer $POSTWIRE_API_KEY"const res = await fetch("https://postwire.io/api/rss/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f/items", {
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/rss/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f/items",
headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
)
print(r.status_code, r.json())