Error Codes
Reference for all Chirpie API error codes and how to handle them.
Error Response Format
All errors follow this format:
{
"error": {
"code": "error_code",
"message": "Human-readable description of what went wrong"
}
}Error Codes
| HTTP Status | Code | Description | What to Do |
|---|---|---|---|
400 | bad_request | Invalid request body, missing fields, or validation error | Check the message for specifics. Fix the request and retry. |
400 | validation_error | A comment reply with no text, or text over the target platform's character limit | The message names the limit and what you sent. Shorten the reply and retry. Nothing was published. |
400 | unsupported_media | A media_urls item the target platform cannot accept: a video on a platform that takes images only, a format it cannot decode, or a file over its size limit | Remove or replace the item, or send it to a platform that takes it. Nothing was posted. |
400 | first_comment_unsupported | first_comment was sent to an account whose platform has no first comment | Remove first_comment, or send it to an X, Threads, Instagram or Facebook account. The message names the platform that refused. Nothing was posted. |
400 | configuration_unsupported | A configuration option the platform or the chosen placement does not carry | The message names the field and what to do: remove it, or change the placement. Nothing was posted. See Publishing options. |
400 | bluesky_app_password_required | A Bluesky connect whose app_password is not shaped like a Bluesky app password (xxxx-xxxx-xxxx-xxxx), most often the account password | Create an app password in Bluesky under Settings, Privacy and Security, App Passwords and send that. Nothing was sent to Bluesky, and the message never repeats the value. |
400 | mastodon_instance_invalid | A Mastodon connect whose instance_url names no server, or names one over plain HTTP | Send the server (mastodon.social), your handle (@you@mastodon.social) or your profile URL (https://mastodon.social/@you). |
400 | idempotency_key_invalid | The Idempotency-Key header was blank, or longer than 255 characters | Send a value you can repeat, 255 characters or less. Hash a longer one rather than sending it whole. |
400 | bad_request | A schedule_at that is the right shape but not a real moment, such as 2026-02-30T09:30 or 2026-01-01T25:00 | The message says so. Check the day of the month and the hour; it used to roll over to another day. |
401 | unauthorized | Missing, invalid, expired, or revoked API key | Check your Authorization header format: Bearer chirpie_sk_... |
402 | x_link_posts_require_paid_plan | An X post containing a link was sent from a Free-plan account | Upgrade to a paid plan, remove the link, or connect an X account using your own X API keys. |
402 | comment_read_budget_exceeded | Your plan's monthly comment allowance is spent and there is no stored snapshot to answer with | Upgrade your plan, or wait for the next month. Posts whose comments are already stored keep listing as normal. |
403 | insufficient_scope | The API key does not hold the scope this route needs, or POST /api/v1/keys was asked to grant a scope the key does not itself hold | The message names the missing scope. Use a key that has it, or mint one from an unrestricted key. See Scopes. |
404 | not_found | Resource doesn't exist or belongs to another user | Verify the ID. Posts/accounts are scoped to your user. |
404 | first_comment_not_found | A retry of a first comment on a post that has none | Add one with PATCH /api/v1/posts/:id while the post is still unpublished, or send a new post with first_comment. |
409 | post_not_editable | The post is publishing, published, failed or deleted, or the draft has already been promoted, so it can no longer be edited | Read it back with GET /api/v1/posts/:id to see where it ended up. |
409 | first_comment_not_retryable | The first comment is already posted, the post has not published yet, or another attempt is in flight | Read the post back with GET /api/v1/posts/:id: first_comment.status says which. Only a failed first comment on a published post can be retried. |
409 | comment_permission_required | The account needs re-authorizing before Chirpie can read or manage its comments | Reconnect the account from the dashboard. Posting and analytics keep working meanwhile. |
409 | idempotency_in_progress | The same Idempotency-Key is still being worked on | Retry the identical request to collect the replay. Chirpie does not queue you behind the first attempt. See Idempotency. |
422 | idempotency_key_reused | The same Idempotency-Key arrived with a different method, path or body | Use a fresh key for a different request. A key is bound to the exact request it was first used with. See Idempotency. |
429 | rate_limited | The burst rate limit on your API key. The ceiling is your plan's, from 120 requests a minute on Free to 1,200 on Pro and above | Retry after Retry-After. It is always present on this one. Pace on X-RateLimit-Remaining to stay ahead of it. |
429 | account_limit_reached | Connecting or activating an account would exceed your plan's account limit | Deactivate another account, or upgrade your plan. Waiting does not help, so this response carries no Retry-After. |
429 | analytics_refresh_rate_limited | ?refresh=true on analytics inside the 5-minute floor for that post | Retry after Retry-After, or drop refresh and read the stored numbers, which are always available. |
429 | comment_sync_rate_limited | The platform is rate limiting comment reads and there is no stored snapshot to answer with | Retry after Retry-After. A post whose comments are already stored keeps listing from the snapshot. |
429 | usage_limit_exceeded | On Free, the monthly post quota is spent (paid plans keep publishing past it and are billed per post). On any plan, the monthly scheduled-post quota is spent. A comment reply is published to the platform, so it counts as a post too | Upgrade your plan, or wait for the next month. Waiting a few seconds does not help, so this response carries no Retry-After. |
500 | internal_error | Unexpected server error, including one while starting an account connection | Retry after a moment. If persistent, contact support. |
501 | platform_coming_soon | The platform is recognised but cannot be connected yet | Connect one of the available platforms for now. |
501 | platform_unsupported | The platform is not supported | Use one of the supported platforms. |
501 | comments_unsupported | The platform exposes no comment read for this post | Read the comments on the platform itself. See the Comments API table. |
501 | comment_action_unsupported | The platform allows reading comments but not this action | Check meta.capabilities on the listing before offering the control. |
501 | delete_unsupported | Chirpie cannot delete a published post on this platform, on this account (an Instagram account connected through Instagram rather than via Facebook), or on this placement (a Facebook Page story) | Delete it in the platform's own app. The post stays in Chirpie with its history; POST /api/v1/posts/{id}/hide keeps it out of your listings. |
502 | upstream_error | Platform API returned an error, on a post, a thread, a delete, or while starting an account connection (POST /api/v1/accounts) | Often temporary. Scheduled posts auto-retry. For immediate posts, deletes and connects, retry after a delay. Nothing partial is left behind either way. |
429 | first_comment_rate_limited | The platform is rate limiting posts from this account, so the first comment could not go out | The post itself published and is untouched. Try the comment again shortly. |
502 | first_comment_failed | The platform refused a retry of a first comment | The post is untouched and still published. The message says what the platform answered. Retry after a delay. The text cannot be changed once the post is out, so post the comment yourself if the platform keeps refusing it. |
502 | thread_rollback_incomplete | A thread failed part-way and Chirpie could not remove every post it had already published | Read error.thread_rollback.still_live for the posts that are still up, delete them, then retry. Retrying without doing that publishes them twice. |
503 | media_storage_failed | The media was downloaded but could not be stored | Temporary on our side. Retry in a moment; nothing was published and nothing was charged. |
Common Scenarios
"Invalid API key"
{ "error": { "code": "unauthorized", "message": "Invalid API key" } }The key doesn't match any active key in our database. It may have been revoked or mistyped.
"API key has been revoked"
{ "error": { "code": "unauthorized", "message": "API key has been revoked" } }This key was explicitly revoked. Create a new key from the dashboard or API.
"This API key does not have the ... scope"
{
"error": {
"code": "insufficient_scope",
"message": "This API key does not have the posts:write scope. Create a key with posts:write and use that instead."
}
}A key created with scopes can only reach the routes those scopes cover. The message always
names the one that is missing, so the fix is to use a key that holds it.
The same 403 comes back from POST /api/v1/keys when the key you are minting with is
asked to grant a scope it does not itself hold: a key can never create a wider key than
itself. Omitting scopes is not a way around it: the new key then inherits the caller's own
list, so only a full-access key (or the dashboard) mints one. Every key that predates scopes
has full access and is unaffected by any of this.
See Scopes.
"Retrying a post without publishing it twice"
Send an Idempotency-Key header on the original request, then send the identical request
again to retry it. Three things can come back:
{ "error": { "code": "idempotency_in_progress", "message": "Idempotency-Key 'post-2026-11-01' is still being processed. Retry in a moment to get the result of the first request." } }A 409. The first attempt is still running, and Chirpie answers rather than holding your
connection open behind it. Retry the same request in a moment and you get its stored answer.
{ "error": { "code": "idempotency_key_reused", "message": "Idempotency-Key 'post-2026-11-01' was already used for a different request. Use a new key for a new request, or send the original request again to get its result back." } }A 422. The key is bound to the method, path and body it was first used with, so reusing it
for something else is refused rather than replaying the wrong answer. Generate a new key per
distinct request.
A successful replay is not an error at all: it is the original response, byte for byte, with
Idempotent-Replay: true on it. See Idempotency.
"Monthly post limit reached"
{ "error": { "code": "usage_limit_exceeded", "message": "Monthly post limit reached (50/50). Upgrade your plan." } }You've hit the Free plan's monthly post quota. Upgrade your plan, or wait for the quota to
reset on the 1st of the month (UTC). Paid plans never get this for the post quota: posts past
the allowance keep publishing and are billed per post. A thread that would cross the line names the arithmetic instead:
Monthly post limit would be exceeded (48 + 5 > 50). Upgrade your plan. The scheduled
quota has its own pair of messages, with Monthly scheduled post limit in place of
Monthly post limit.
Every authenticated /api/v1/* response carries X-RateLimit-Limit, X-RateLimit-Remaining
and X-RateLimit-Reset (unix seconds). A 429 from the burst limiter also
carries Retry-After, in seconds. See Rate Limits for the
full table and a retry recipe.
"upstream_error" from a platform
{ "error": { "code": "upstream_error", "message": "Mastodon (mastodon.social) returned 503. Try again in a moment." } }The platform itself failed. Chirpie passes on the platform's own explanation when it gave one, and otherwise names the service and the status it answered with. The message is always short: a platform's raw error page is never passed through.
Three guarantees come with it, whatever the platform said:
- A thread is all or nothing. If any post of a thread fails, every post that had
already published is deleted from the platform, no rows are kept, and the whole
thread's quota is refunded. The error says so, and names what it removed, in
error.thread_rollback. See A thread that failed part-way. - A failed post is never charged. The reserved quota is returned on any failure.
- A failed delete changes nothing. The post keeps its status and stays in your
listings, so retrying the same
DELETEis safe. Chirpie never reports a post as deleted while it is still live on the platform.
{ "error": { "code": "upstream_error", "message": "Could not delete the post from Mastodon. Try again in a moment. The post is still up and Chirpie still has it." } }A thread that failed part-way
A thread is atomic on every platform and on every path that publishes one: immediate, scheduled, promoting a draft, and each account of a multi-account publish. Whatever went wrong, the whole thread's quota comes back.
{
"error": {
"code": "upstream_error",
"message": "X API error: Rate limit exceeded The 2 posts already published were deleted from X, and the thread's quota was refunded.",
"thread_rollback": {
"platform": "x",
"published": 2,
"rolled_back": ["1930000000000000001", "1930000000000000002"],
"still_live": [],
"rollback_supported": true,
"quota_refunded": true
}
}
}The code tells you whether a retry is safe.
upstream_error means still_live is empty: nothing of the thread is on the
platform, and repeating the same request is safe.
thread_rollback_incomplete means Chirpie could not remove everything. Those
posts are really still up, named with their ids and their public URLs, and
repeating the request would publish them a second time.
{
"error": {
"code": "thread_rollback_incomplete",
"message": "X API error: Rate limit exceeded The thread's quota was refunded, but X refused to delete 1 post that had already published. It is listed in this error and is still live.",
"thread_rollback": {
"platform": "x",
"published": 2,
"rolled_back": ["1930000000000000001"],
"still_live": [
{
"thread_order": 1,
"platform_post_id": "1930000000000000002",
"platform_post_url": "https://x.com/you/status/1930000000000000002"
}
],
"rollback_supported": true,
"quota_refunded": true
}
}
}rollback_supported: false means Chirpie cannot delete a published post for
that account, so nothing could be removed. That is always the case on TikTok,
and on an Instagram account connected through Instagram rather than
via Facebook.
See the delete table.
A scheduled thread that fails is retried three times before Chirpie gives up, unless the rollback left posts live: then it fails immediately and the reason names them, because a retry would publish them twice.
"Account limit reached"
Two endpoints raise it, each with its own instruction.
{ "error": { "code": "account_limit_reached", "message": "Account limit reached (1/1). Upgrade your plan to connect more accounts." } }From POST /api/v1/accounts, when a new connection would take you past your
plan's account limit.
{ "error": { "code": "account_limit_reached", "message": "Account limit reached (1/1). Deactivate another account or upgrade your plan to activate this one." } }From PATCH /api/v1/accounts/{id}, when activating an account would.
It is a 429 but it is not a rate limit: waiting frees nothing, so there is no
Retry-After. Deactivate an account you are not using, or upgrade.
Reconnecting a platform you already hold an account on does not need a free slot, so re-authorizing an expired connection always works, even on the Free plan.
Comment errors
{
"error": {
"code": "comment_action_unsupported",
"message": "X has no way to hide a reply. You can reply to it, or delete a reply you wrote."
}
}The platform has no endpoint for what you asked. The message names what it does allow.
Every listing carries meta.capabilities for that post, so this is avoidable: check
hide, delete_own and delete_any before offering a control.
{
"error": {
"code": "comment_permission_required",
"message": "Reconnect your Facebook Page to manage comments."
}
}The account needs re-authorizing before Chirpie can read or manage its comments.
Reconnect it from the dashboard. Posting,
scheduling and analytics keep working meanwhile, and the same sentence appears in
meta.sync.reason on a listing, so you do not need a second request to show it.
A refused refresh is deliberately not an error. A listing whose refresh was refused by
the refresh window, your plan, the platform's rate limit, or a connection that needs
re-authorizing still returns 200 with the stored comments and a meta.sync.status of
budget, plan, rate_limited or permission: the read succeeded, and the snapshot is
what you asked for.
There is one exception, and it is the same in all three cases: when you asked for a
refresh explicitly with sync=true and the post has no stored comments at all, an empty
200 would read as "this post has no comments", so the refusal is returned instead. That
is 402 comment_read_budget_exceeded, 429 comment_sync_rate_limited or 409 comment_permission_required, each carrying the sentence that would otherwise have been in
meta.sync.reason. See the Comments API.
first_comment_unsupported
{
"error": {
"code": "first_comment_unsupported",
"message": "Bluesky does not support a first comment. Remove first_comment, or send it to X, Threads, Instagram and Facebook accounts only."
}
}A first comment is published under the post the moment it goes out, and only X, Threads, Instagram and Facebook have one. The request is refused before anything is published, so nothing goes out and no quota is taken. On a fan-out, one account that cannot take a first comment refuses the whole request, so name only accounts that take one on a request that carries one.
configuration_unsupported
{
"error": {
"code": "configuration_unsupported",
"message": "Instagram stories do not take cover. Remove configuration.instagram.cover, or change the placement."
}
}Publishing options are never dropped
quietly. This code covers a configuration block keyed on a platform that
takes none, one keyed on a platform no account in the request uses, a field the
chosen placement does not carry, text or a first_comment on a story, and an
Instagram story or reel or a Facebook Page story asked for on a thread, which
are each a single post. The message names the field and what to do about it,
and nothing is published. The placements and their options are on the
Instagram and
Facebook pages.
"Unknown platform"
{ "error": { "code": "bad_request", "message": "Unknown platform 'twitter'. Supported: x, bluesky, linkedin, threads, mastodon, instagram, facebook, telegram." } }POST /api/v1/accounts rejects a platform value it does not recognise rather
than falling through to a default. Omitting platform entirely still means x.
"Unknown Instagram connect route"
{ "error": { "code": "bad_request", "message": "Unknown Instagram connect route 'meta'. Supported: instagram, facebook." } }An Instagram account connects either by signing in with Instagram ("via": "instagram", the default) or by signing in with Facebook ("via": "facebook"). Anything else is refused before the flow starts. See
Connect an Instagram account.
bluesky_app_password_required
{ "error": { "code": "bluesky_app_password_required", "message": "Bluesky needs an app password, not your account password. Create one in Bluesky under Settings, Privacy and Security, App Passwords (https://bsky.app/settings/app-passwords). It looks like xxxx-xxxx-xxxx-xxxx." } }Bluesky's own sign-in accepts the account password as readily as an app password, and the account password gives full control of the account, so Chirpie refuses anything not shaped like an app password before Bluesky is asked. Create one in Bluesky under Settings, Privacy and Security, App Passwords. It is four groups of four lowercase letters or digits joined by hyphens. The CLI refuses the same values locally, with the same code and message.
mastodon_instance_invalid
{ "error": { "code": "mastodon_instance_invalid", "message": "Enter your Mastodon server (mastodon.social), your handle (@you@mastodon.social) or your profile URL (https://mastodon.social/@you)." } }instance_url accepts any of those forms and keeps only the server. This
answer means none could be read from what was sent (a bare word such as
mastodon, or an http:// address).
"Platform coming soon"
{ "error": { "code": "platform_coming_soon", "message": "pinterest support is coming soon." } }The platform is recognised but cannot be connected yet. platform_unsupported,
also a 501, means the platform is not supported at all.
"Missing required field"
{ "error": { "code": "bad_request", "message": "account_id and text are required" } }Missing fields are named. One missing field reads account_id is required, and a
field inside a thread reads posts[0].text is required.
"X link posts require a paid plan"
{
"error": {
"code": "x_link_posts_require_paid_plan",
"message": "X charges $0.20 per post containing a link, so link posts to X aren't available on the Free plan..."
}
}X bills API operators $0.20 for every post containing a URL. On the Free plan these posts are rejected before any quota is consumed. Options:
- Upgrade to a paid plan. X link posts are billed at $0.25 each, on top of your monthly allowance.
- Remove the link from the post text. Bare domains (
chirpie.ai) count as links, because X auto-links them. - Connect an X account using your own X API credentials. Those accounts are exempt and you pay X directly.
Posts to every other connected platform are never affected.
"Unknown field"
{ "error": { "code": "bad_request", "message": "Unknown field 'scheduled_at'. Did you mean 'schedule_at'?" } }POST /api/v1/posts, POST /api/v1/threads and PATCH /api/v1/posts/:id
reject any top-level field they do not recognise, rather than ignoring it.
The most common cause is scheduled_at. That is the field name in the
response; the field you send is schedule_at. Sending scheduled_at
returns a 400 and publishes nothing, so a round-tripped post object can never
go out immediately by mistake. The message names the field you probably meant.
Accepted fields:
POST /api/v1/posts:account_id,account_ids,account_configurations,text,media,media_ids,media_urls,first_comment,configuration,schedule_at,timezone,draftPOST /api/v1/threads:account_id,account_ids,account_configurations,posts,first_comment,configuration,schedule_at,timezone,draftPATCH /api/v1/posts/:id:text,media,media_ids,media_urls,first_comment,configuration,schedule_at,timezone,draft,publish
If you are round-tripping a post object you read back from the API, send only these fields rather than the whole object.
"Account not found or not active"
{ "error": { "code": "not_found", "message": "Account not found or not active" } }The account_id doesn't exist, belongs to another user, or the credentials have expired. For OAuth-based accounts (X, LinkedIn, Threads, Mastodon, Instagram, Facebook), re-authorize from the dashboard. For Bluesky, generate a new app password and reconnect. For Telegram, verify your bot token and chat ID.
Platform Errors
When a platform rejects a post, Chirpie returns 502 upstream_error with the
platform's own message attached:
{ "error": { "code": "upstream_error", "message": "X API error: Rate limit exceeded" } }The prefix names the platform (X, Bluesky, LinkedIn, Threads, Mastodon,
Instagram, Facebook, Telegram). Scheduled posts retry automatically, up to three
times. For immediate posts, retry after a delay.
| Platform | Common causes | Fix |
|---|---|---|
| X | Rate limit (~200 posts/day), suspended account, content policy | Wait, or check the account on X |
X (with media_urls) | Reconnect your X account to upload media.: X has not granted permission to upload media on that account | Reconnect the account from the dashboard. Text posts keep working meanwhile |
| Bluesky | Revoked app password, expired session, rate limit | Generate a new app password and reconnect |
| Rate limit (~100 posts/day per member), expired token | Re-authorize from the dashboard | |
| Threads | Rate limit (250 posts/24h), expired Meta token | Re-authorize from the dashboard |
Threads (on DELETE) | Reconnect your Threads account to delete posts.: Threads has not granted permission to delete posts on that account | Reconnect the account from the dashboard. Posting, scheduling and analytics keep working meanwhile |
| Mastodon | Rate limit (300 posts/3h by default), revoked token | Re-authorize from the dashboard |
| Rate limit (50 posts/24h), expired token | Re-authorize from the dashboard | |
| Rate limit (4,800 calls/24h per Page), revoked Page permissions | Re-authorize from the dashboard | |
| Telegram | Invalid bot token, bot not an admin of the chat, wrong chat ID | Verify the bot token and chat ID |
Each platform's own posting limits are listed on Rate Limits.
Facebook Returns No Pages
Facebook only shares the Pages you tick in its authorization window, so a connect can finish with fewer Pages than you manage. The message names which case you hit and what to change. See If a Page is missing after connecting for the full table.
Platform Validation Errors
These are 400 bad_request, raised before anything reaches the platform.
| Message | Meaning |
|---|---|
Text must be 300 characters or less (Bluesky account). Got 412. | The text is longer than the account's limit. Shorten it, or send a thread |
bluesky accepts at most 4 media items per post (got 6). | Too many items in media_urls for that platform |
Instagram requires at least one image. Text-only posts are not supported. | Instagram posts must carry media |
Scheduled posts must be at least 5 minutes apart for the same account. | Space the schedule out. See Scheduling |
Per-platform character limits and media caps are listed on the Posts API page.
unsupported_media
400 unsupported_media means the media cannot be posted to that platform, so nothing
was posted at all. Media is never silently dropped: if the request is accepted, every
item in media_urls is published.
| Message | Meaning |
|---|---|
Bluesky does not support video. Remove the video or post it to X, Mastodon or Telegram. | The platform takes images only |
X accepts one video per post, on its own. Send the video by itself. | A video cannot be mixed with images |
LinkedIn does not support image/webp. Supported image formats: image/jpeg, image/png, image/gif. | Convert the image, or send it to a platform that takes the format |
The media is 2.4 MB. Bluesky accepts images up to 977 KB. | The file is over that platform's own limit |
The check runs twice: once on the URLs, before anything is downloaded, and again on the real content type and byte size once it has been. A scheduled post is refused at the moment it is created, not at publish time. The Posts API table is the full set of rules.
A media file the platform itself refuses (an image below Instagram's minimum width, say)
still comes back as 502 upstream_error, carrying the platform's own explanation.
Telegram analytics
Telegram exposes no metrics API, so
GET /api/v1/analytics/posts/:id on a Telegram post returns
502 upstream_error with Could not fetch metrics rather than a row of zeros.
Rate Limits & Pricing
What Chirpie costs, monthly post quotas, per-post overage rates, the X link-post charge, burst limits, and how to handle rate limiting.
Post to X from Claude
Let Claude post to X (Twitter) through Chirpie, added from the Claude Connectors Directory, by the hosted MCP endpoint, or in Claude Code.