Errors, rate limits and idempotency
Every error has the same shape and a machine-readable code, so a flow can branch on it without parsing English.
Error shape
{
"error": "not connected: tiktok. Connect it in Accounts, or post only to bluesky.",
"code": "not_connected",
"platforms": [
"tiktok"
]
}| Status | When |
|---|---|
400 | The request is wrong: a missing field, a past run_at, a network not connected (code not_connected), media required (media_required) or unreachable (media_unreachable), the plan's queue full (queue_full), a temporary-mailbox signup (disposable_email). |
401 | No API key, or a key that does not exist or was revoked. |
402 | A feature the plan does not include (plan_feature), with the plan that does. |
403 | Email not verified, or a key that cannot publish. account_suspended: the workspace is suspended. media_upload_limit: the month's media uploads (fair use) are used up. |
404 | Not found (not_found, brand_not_found). Ids that are not valid answer 404 too. |
409 | ambiguous_brand (say which brand), duplicate_post (see idempotency), or a scheduled post that is already publishing. |
413 | A file too large (too_large, or over the plan's file size: file_too_large_for_plan), or one that must be uploaded in parts (use_multipart). |
429 | Too many requests (rate_limited, per endpoint or per key a minute), too many publishes running at once (too_many_concurrent), too many new accounts from one network or browser (signup_limit), a plan's AI drafts a day used up (ai_daily_limit), or the month's AI drafts and any AI credits used up (ai_month_limit, with buy_ai_credits links and the plan above; back on the 1st). |
503 | Something on PostWire's side is not available (auth_unavailable, ai_unavailable, webhooks_unavailable). Retry. |
Result codes per network
In results[] of POST /api/post, in a scheduled post's results and in post.failed webhooks. The list is open: new codes can appear, so treat an unknown code as a failure with the text in error.
| code | Means | What to do |
|---|---|---|
not_connected | The network is not connected (in that brand). | Connect it, or leave it out of the request. |
duplicate_recent | A recurring post's occurrence was not sent to this network: the same text went out there in the last guard_days days. | Add variants to the recurring post, or lower its guard_days. |
ambiguous_brand | The network is connected in several brands and no brand_id was sent (whole request, 409). | Send brand_id. |
media_required | TikTok, YouTube and Instagram need a video or photos; a Reel needs a video. | Add video_url, photo_url or media. |
media_unreachable | A media link could not be downloaded (private, expired, deleted, or a web page). Checked once before anything is sent. | Use a public direct link, or upload the file. |
bad_media | The media list does not fit the network (too many items, a type it does not take). | Follow the error text. |
caption_is_json | The text was a JSON object, not the words of the post. | Send the post's words as text. |
empty_text | Nothing to publish. | Send text, a per_platform draft, or media. |
limit_reached | The month's posts on the plan are used up (on Pro, Agency and Scale: the published fair-use ceiling, with fair_use: true). Comes with upgrade_url. | Wait for the 1st, or upgrade. |
daily_limit | The plan's posts for today (UTC) are used up — fair use, a post counts per network (limit, resets_at; upgrade_url when a bigger plan allows more). | Post it after 00:00 UTC, or schedule it. |
throttled | This workspace published far more this hour than its usual hour (over 10 times its median, never under a quarter of its day), so publishing is paused until the hour ends (retry_after, resets_at). Scheduled posts keep their place. | Wait for the hour to end; if the volume is expected, write to support. |
media_egress_limit | The month's media sent to networks is used up (fair use; each uploaded file counts once per network it goes to). Text posts and media hosted elsewhere still go out. | Wait for the 1st, link media hosted elsewhere, or upgrade. |
account_suspended | 403, whole request. The workspace is suspended by PostWire: nothing is published, from the API or the queue. | Write to support. |
networks_per_post | held: true. On Free a post goes out to 2 networks; this one was written and not sent, and spent nothing. | Upgrade, or send it in another post. |
rate_limited | The network asked to wait (retry_after seconds) and the post could not be queued for after it. | Post it again after retry_after. |
rate_limited_deferred | ok: true, status scheduled. The network asked to wait longer than the request had; this network was queued for right after the wait. | Nothing. Follow schedule_id. |
timed_out | status: unknown. The network did not answer before PostWire had to reply. | Check the network before posting again. |
x_credits_required | X: not enough X credits for this post (for a thread: for the whole thread). Nothing was sent to X. Comes with buy_credits (links) and credits_needed. | Buy top-up credits, or wait for the 1st. |
bad_x_thread | 400, whole request. A post of the X thread or the reply is empty, not text or over 280 characters as X counts them, or the thread has more than 25 posts. Nothing was published. | Shorten or split the post the error names. |
x_paid_plan_required | X: the account is on Free or in its trial. X is on paid plans. Comes with upgrade_url when there is a plan to buy. | Upgrade, or leave X out. |
x_daily_limit | X: the plan's X posts for today are used (Starter 5, Pro 20, Agency 60, Scale 150); every post of a thread counts. | Post it after 00:00 UTC. |
x_paused | X: posting to X is paused on PostWire. No credits were used. | Try again later. |
x_no_answer | X never answered (or answered 500/504): PostWire can't tell whether it went out, so the credits stay used. | Check the X account before posting again. |
pending_approval | 202. The brand's approval rules (or a limit on this key) hold the post for a person; nothing was sent. Comes with id, approval_id, who was notified and review_url. | Wait: GET /api/post/status with the id says when it is approved, rejected or published. |
key_network_not_allowed | 403, whole request. This API key or AI app may not post to that network (a limit set in the dashboard). Nothing was published. | Post to an allowed network, or ask the owner to change the key's limits. |
key_brand_not_allowed | 403, whole request. This API key or AI app may not post for that brand. | Use an allowed brand. |
key_monthly_cap_reached | 403, whole request. This API key or AI app used its posts for the month (cap, used, resets_on). | Wait for the 1st (UTC), or ask the owner to raise it. |
approval_requires_human | 403. Approving, rejecting and changing approval rules or key limits is for a person in the dashboard (or the emailed link), never an API key or an AI connector. | Open the review link in a browser. |
governance_unavailable | 503. The approval rules or key limits could not be checked, so nothing was published. | Try again in a minute. |
role_forbidden | 403, whole request. Your role in this workspace (or the role of the member who created this key) can't do this — e.g. a Viewer or an Approver publishing. Says which role and what it can't do. | Ask an admin, or use a key of a member with that role. |
seat_read_only | 403. The workspace has more members than its plan's seats (after a downgrade): the latest members are read-only until a seat is free. | The owner upgrades, or someone leaves. |
brand_restricted | 403. You work with only some brands of this workspace and this covers all of them. | Ask an admin. |
workspace_forbidden | 403. Not a member of that workspace (any more), or an API key asked for another workspace (X-PostWire-Workspace). | Use a key created inside that workspace. |
key_owner_left | 401. The member who created this key or AI app left the workspace, so it no longer works. | Create a new key. |
seat_limit | 403. Every seat of the plan is taken (members plus open invitations). Comes with the next plan's checkout link. | Remove a member, cancel an invitation, or upgrade. |
team_requires_human | 403. Inviting, changing roles, removing and transferring are for a person in the dashboard, never an API key or an AI connector. | Do it in Settings → Team. |
A failed network also carries what and fix: the same plain-words explanation the failure emails use.
Request rate limits
Over a limit the API answers 429 with a Retry-After header (seconds) and the same number in the body:
{
"error": "too many requests — please slow down and retry in 60 seconds",
"code": "rate_limited",
"retry_after": 60
}| Endpoint | Limit |
|---|---|
Every authenticated call | per API key (or connected AI app), a minute: Free 60, Starter 120, Pro 300, Agency 600, Scale 1,200 |
POST /api/post | 60 a minute per account; publishes running at once per workspace: Free 2, Starter 3, Pro 5, Agency 10, Scale 20 (429 too_many_concurrent) |
POST /api/generate | 8 a minute per account, plus the plan's AI drafts a day and a month |
POST /api/week | 3 an hour per account (each counts as one AI draft) |
POST /api/bulk | 60 dry runs and 20 uploads an hour per account |
POST /api/rss/preview | 30 an hour per account; POST /api/rss/{id}/check 6 an hour per feed |
POST /api/media/upload-url | 60 an hour per account |
POST /api/media/upload-link | 30 an hour per account |
POST /api/media/video | 10 an hour per account; 10 a day on Free, 60 on paid plans |
POST /api/keys | 20 a day per account |
POST /api/webhooks | 20 a day per account; POST …/test 30 an hour |
GET /api/connect/{platform}/health | 240 an hour per account |
POST /api/signup | 5 an hour per IP address, 3 a day per email; new accounts: 10 a day per network and 3 a day per browser (429 signup_limit); temporary-mailbox domains are refused (400 disposable_email) |
POST /api/approvals/link… | 120 every 10 minutes per IP address; POST /api/approvals/{id}/decision 120 every 10 minutes per account |
POST /api/team/invites | 30 invitations (and resends) a day per workspace; each one resent at most once an hour |
POST /api/invites/… | 60 every 10 minutes per IP address; 4 codes per 15 minutes per invitation |
Plan limits (posts a month and a day, AI drafts a day and a month, brands, networks per post, the queue, media a month) are not rate limits: they answer with their own code and an upgrade link, and GET /api/usage shows each one. See plans and limits and fair use.
Network rate limits
Networks have their own limits. Telegram allows about one message a second per chat and 20 a minute in a group. PostWire spaces posts to the same chat, waits out a Telegram 429 when the request has time for it, and otherwise queues that network for right after the wait: its result is ok: true, status: "scheduled", deferred: true, code rate_limited_deferred, with the schedule_id and run_at. Nothing is lost, and the queued post is counted and sent to your webhooks when it goes out.
Idempotency: retries without duplicates
A publish cannot be undone, and n8n, Make, Zapier and agents all retry. So POST /api/post refuses the same post twice:
- Send
idempotency_key(any string up to 200 characters). The same key on the same account within 24 hours answers 409duplicate_postand publishes nothing. - Without a key, an identical request (same networks, text, drafts, media and brand) within 2 minutes answers 409
duplicate_post. - When nothing went out (every network failed), the lock is released at once, so the retry that fixes the problem is not refused.
- The key travels in the
post.publishedandpost.failedwebhooks, so you can match events to your request.
Request
{
"platforms": [
"linkedin"
],
"text": "Doors open at 7.",
"idempotency_key": "order-1842-announce"
}The same request again
{
"error": "That idempotency_key was already used for a post on this account in the last 24 hours. Nothing was published again.",
"code": "duplicate_post"
}