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.

View as MarkdownOpenAPIBase URL https://postwire.io

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.

Bulk upload: check, then schedule many posts at once (CSV or rows)

POST/api/bulk

A 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)

FieldTypeDescription
csvstringThe CSV text, first line = column names. Up to 3 MB.
rowsarray of objectInstead of csv: objects with the same column names.
dry_runbooleanDefault true.
timezonestringIANA timezone for dates without one.
brand_idstringDefault brand (id or name) for rows without a brand column.
fit_long_textbooleanA text over a network's limit is cut at a sentence or word when published (a warning), instead of an error. Default false.
date_formatstringHow to read 12/10/2026. One of: ymd, dmy, mdy. Default "ymd".
allow_duplicatebooleanDefault false.
check_mediabooleanCheck that each media link answers (each link once, up to 20 seconds in all). Default true.
batch_idstringYour own id for the upload (6-64 letters, digits, - or _). Default: a hash of the file.

Responses

StatusMeans
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 }
400bad_csv (with line), bad_timezone, bad_request
403bulk_rows_limit (with the plan that takes the upload), email not verified
409bulk_already_scheduled
422bulk_rows_invalid: nothing was scheduled; each row says what to fix
503bulk_store_failed / governance_unavailable: nothing was scheduled
curl
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
}'
Node
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());
Python
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

JSON
{
  "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

GET/api/bulk/template.csv

A CSV with every column and three example rows (dates a week from today). No key needed.

No API key needed.

Responses

StatusMeans
200text/csv
curl
curl "https://postwire.io/api/bulk/template.csv"
Node
const res = await fetch("https://postwire.io/api/bulk/template.csv", {
  method: "GET",
});
console.log(res.status, await res.json());
Python
import os, requests

r = requests.get(
    "https://postwire.io/api/bulk/template.csv",
)
print(r.status_code, r.json())

Undo a bulk upload

DELETE/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

FieldTypeDescription
id requiredpath, string

Responses

StatusMeans
200{ ok, batch_id, canceled, already_out, message }
404not_found
curl
curl -X DELETE "https://postwire.io/api/bulk/5681dfcaa8a49e2bd2a6" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY"
Node
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());
Python
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

JSON
{
  "ok": true,
  "batch_id": "5681dfcaa8a49e2bd2a6",
  "canceled": 12,
  "already_out": 0
}

Set up a recurring post

POST/api/recurring

Every 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)

FieldTypeDescription
platforms requiredarray of stringOne of: tiktok, instagram, youtube, linkedin, facebook, bluesky, mastodon, telegram, discord, x.
textstringThe post, when there is one text.
variantsarray of anyTexts used in turn: occurrence n uses variant ((n-1) mod count) + 1. Up to 50 items.
rule requiredobjectWhen 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 requiredstringOne of: daily, weekly, monthly.
rule.intervalintegerEvery N days / weeks / months, counted from start. Default 1. 1 to 52.
rule.daysarray of stringweekly: the weekdays (default: the start date's). One of: mon, tue, wed, thu, fri, sat, sun.
rule.month_daysarray of integermonthly: days of the month; -1 = the last day. 31 falls on the last day of shorter months.
rule.timesarray of string24-hour HH:MM, 1-4 a day. Default 09:00. Up to 4 items.
rule.timezonestringIANA name. Default: the brand's queue timezone, else UTC.
rule.startstring (date)First day (default today); also the anchor of every N.
rule.untilstring (date)Last day, inclusive.
rule.countintegerStop after N occurrences. 1 to 1000.
guard_daysintegerWhen 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_platformobject
mediaarray of object
photo_urlstring
video_urlstring
optionsobject
titlestring
labelstringUp to 120 characters.
brand_idstring (uuid)

Responses

StatusMeans
201{ ok, recurring, queued_now, warnings, message }
400bad_rule, not_connected, text_required, too_long, media_required
403plan_feature, recurring_limit, x_paid_plan_required
503automation_unavailable
curl
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"
}'
Node
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());
Python
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

JSON
{
  "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

POST/api/recurring/preview

The 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)

FieldTypeDescription
ruleobjectWhen 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 requiredstringOne of: daily, weekly, monthly.
rule.intervalintegerEvery N days / weeks / months, counted from start. Default 1. 1 to 52.
rule.daysarray of stringweekly: the weekdays (default: the start date's). One of: mon, tue, wed, thu, fri, sat, sun.
rule.month_daysarray of integermonthly: days of the month; -1 = the last day. 31 falls on the last day of shorter months.
rule.timesarray of string24-hour HH:MM, 1-4 a day. Default 09:00. Up to 4 items.
rule.timezonestringIANA name. Default: the brand's queue timezone, else UTC.
rule.startstring (date)First day (default today); also the anchor of every N.
rule.untilstring (date)Last day, inclusive.
rule.countintegerStop after N occurrences. 1 to 1000.
variantsarray of string
guard_daysinteger
brand_idstring
limitinteger

Responses

StatusMeans
200{ rule, describe, next: [{ at, local, n, variant }], warnings }
400bad_rule
curl
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."
  ]
}'
Node
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());
Python
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

JSON
{
  "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

GET/api/recurring

Each 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

StatusMeans
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
curl "https://postwire.io/api/recurring" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY"
Node
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());
Python
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

GET/api/recurring/{id}

Needs an API key: Authorization: Bearer pw_live_….

Parameters

FieldTypeDescription
id requiredpath, string (uuid)

Responses

StatusMeans
200{ recurring (with next), history: [{ schedule_id, at, n, status, error }], warnings }
404not_found
curl
curl "https://postwire.io/api/recurring/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY"
Node
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());
Python
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

PATCH/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

FieldTypeDescription
id requiredpath, string (uuid)

Body (JSON)

FieldTypeDescription
platformsarray of stringOne of: tiktok, instagram, youtube, linkedin, facebook, bluesky, mastodon, telegram, discord, x.
textstringThe post, when there is one text.
variantsarray of anyTexts used in turn: occurrence n uses variant ((n-1) mod count) + 1. Up to 50 items.
ruleobjectWhen 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 requiredstringOne of: daily, weekly, monthly.
rule.intervalintegerEvery N days / weeks / months, counted from start. Default 1. 1 to 52.
rule.daysarray of stringweekly: the weekdays (default: the start date's). One of: mon, tue, wed, thu, fri, sat, sun.
rule.month_daysarray of integermonthly: days of the month; -1 = the last day. 31 falls on the last day of shorter months.
rule.timesarray of string24-hour HH:MM, 1-4 a day. Default 09:00. Up to 4 items.
rule.timezonestringIANA name. Default: the brand's queue timezone, else UTC.
rule.startstring (date)First day (default today); also the anchor of every N.
rule.untilstring (date)Last day, inclusive.
rule.countintegerStop after N occurrences. 1 to 1000.
guard_daysintegerWhen 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_platformobject
mediaarray of object
photo_urlstring
video_urlstring
optionsobject
titlestring
labelstringUp to 120 characters.
brand_idstring (uuid)
statusstringOne of: active, paused.

Responses

StatusMeans
200{ ok, recurring, replaced, queued_now, warnings }
404not_found
curl
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"
}'
Node
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());
Python
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

DELETE/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

FieldTypeDescription
id requiredpath, string (uuid)
keep_queuedquery, string

Responses

StatusMeans
200{ ok, canceled }
404not_found
curl
curl -X DELETE "https://postwire.io/api/recurring/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY"
Node
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());
Python
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

POST/api/rss/preview

Reads 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)

FieldTypeDescription
url requiredstringRSS 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.
platformsarray of stringOne of: tiktok, instagram, youtube, linkedin, facebook, bluesky, mastodon, telegram, discord, x.
templatestring{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

StatusMeans
200{ ok, url, kind, title, items_in_feed, items, posts: { <network>: { text, trimmed? } }, note }
400feed_url_blocked, bad_feed_url
422not_a_feed, feed_http_error, feed_timeout, feed_too_large, feed_too_many_redirects
curl
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}"
}'
Node
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());
Python
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

POST/api/rss

Feeds 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)

FieldTypeDescription
url requiredstringRSS 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 requiredarray of stringNot 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.
templatestring{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_minutesintegerDefault 60. 30 to 10080.
max_itemsintegerNew items posted per read, oldest first; the rest wait for the next read. Default 3. 1 to 10.
modestringapproval: every new item waits for approval (paid plans). One of: auto, approval. Default "auto".
post_atstringOne of: now, next_slot. Default "now".
include_imagebooleanDefault true.
ai_rewritebooleanWrite 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_latestbooleanAlso post the newest item now. Otherwise the items already in the feed are recorded, not posted. Default false.
enabledboolean
brand_idstring (uuid)

Responses

StatusMeans
201{ ok, feed, first_read: { primed? | posted? | error? }, message }
400feed_url_blocked, bad_feed_url, bad_template, media_required, not_connected
403plan_feature, rss_feed_limit
409rss_feed_exists
503automation_unavailable
curl
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"
}'
Node
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());
Python
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

JSON
{
  "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

GET/api/rss

Needs an API key: Authorization: Bearer pw_live_….

Responses

StatusMeans
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
curl "https://postwire.io/api/rss" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY"
Node
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());
Python
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

PATCH/api/rss/{id}

Needs an API key: Authorization: Bearer pw_live_….

Parameters

FieldTypeDescription
id requiredpath, string (uuid)

Body (JSON)

FieldTypeDescription
urlstringRSS 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.
platformsarray of stringNot 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.
templatestring{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_minutesintegerDefault 60. 30 to 10080.
max_itemsintegerNew items posted per read, oldest first; the rest wait for the next read. Default 3. 1 to 10.
modestringapproval: every new item waits for approval (paid plans). One of: auto, approval. Default "auto".
post_atstringOne of: now, next_slot. Default "now".
include_imagebooleanDefault true.
ai_rewritebooleanWrite 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_latestbooleanAlso post the newest item now. Otherwise the items already in the feed are recorded, not posted. Default false.
enabledboolean
brand_idstring (uuid)

Responses

StatusMeans
200{ ok, feed }
404not_found
curl
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
}'
Node
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());
Python
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

DELETE/api/rss/{id}

Posts it already queued stay in the queue.

Needs an API key: Authorization: Bearer pw_live_….

Parameters

FieldTypeDescription
id requiredpath, string (uuid)

Responses

StatusMeans
200{ ok }
404not_found
curl
curl -X DELETE "https://postwire.io/api/rss/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY"
Node
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());
Python
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

POST/api/rss/{id}/check

Needs an API key: Authorization: Bearer pw_live_….

Parameters

FieldTypeDescription
id requiredpath, string (uuid)

Responses

StatusMeans
200{ ok, result: { new, posted, results } | { not_modified } | { error }, feed }
409feed_busy
4296 an hour per feed
curl
curl -X POST "https://postwire.io/api/rss/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f/check" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY"
Node
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());
Python
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

GET/api/rss/{id}/items

Needs an API key: Authorization: Bearer pw_live_….

Parameters

FieldTypeDescription
id requiredpath, string (uuid)
limitquery, integer

Responses

StatusMeans
200{ feed, items: [{ item_id, title, link, published_at, status: scheduled | pending_approval | skipped | failed, schedule_id, note, seen_at }] }
curl
curl "https://postwire.io/api/rss/7c2a6f0e-5b1d-4c8a-9e3f-2d4b6a8c0e1f/items" \
  -H "Authorization: Bearer $POSTWIRE_API_KEY"
Node
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());
Python
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())