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). |
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. |
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 one that must be uploaded in parts (use_multipart). |
429 | Too many requests (rate_limited), or a plan's daily AI drafts used up. |
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. |
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. Comes with upgrade_url. | Wait for the 1st, or upgrade. |
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. |
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 |
|---|---|
POST /api/post | 60 a minute per account |
POST /api/generate | 8 a minute per account, plus the plan's AI drafts a day |
POST /api/week | 3 an hour per account (each counts as one AI draft) |
POST /api/media/upload-url | 60 an hour per account |
POST /api/media/upload-link | 30 an hour per account |
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 |
Plan limits (posts a month, AI drafts a day, brands, networks per post, scheduled posts at a time) 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.
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"
}