ChirpieDocs

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 StatusCodeDescriptionWhat to Do
400bad_requestInvalid request body, missing fields, or validation errorCheck the message for specifics. Fix the request and retry.
400validation_errorA comment reply with no text, or text over the target platform's character limitThe message names the limit and what you sent. Shorten the reply and retry. Nothing was published.
400unsupported_mediaA 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 limitRemove or replace the item, or send it to a platform that takes it. Nothing was posted.
400first_comment_unsupportedfirst_comment was sent to an account whose platform has no first commentRemove first_comment, or send it to an X, Threads, Instagram or Facebook account. The message names the platform that refused. Nothing was posted.
400configuration_unsupportedA configuration option the platform or the chosen placement does not carryThe message names the field and what to do: remove it, or change the placement. Nothing was posted. See Publishing options.
400bluesky_app_password_requiredA Bluesky connect whose app_password is not shaped like a Bluesky app password (xxxx-xxxx-xxxx-xxxx), most often the account passwordCreate 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.
400mastodon_instance_invalidA Mastodon connect whose instance_url names no server, or names one over plain HTTPSend the server (mastodon.social), your handle (@you@mastodon.social) or your profile URL (https://mastodon.social/@you).
400idempotency_key_invalidThe Idempotency-Key header was blank, or longer than 255 charactersSend a value you can repeat, 255 characters or less. Hash a longer one rather than sending it whole.
400bad_requestA schedule_at that is the right shape but not a real moment, such as 2026-02-30T09:30 or 2026-01-01T25:00The message says so. Check the day of the month and the hour; it used to roll over to another day.
401unauthorizedMissing, invalid, expired, or revoked API keyCheck your Authorization header format: Bearer chirpie_sk_...
402x_link_posts_require_paid_planAn X post containing a link was sent from a Free-plan accountUpgrade to a paid plan, remove the link, or connect an X account using your own X API keys.
402comment_read_budget_exceededYour plan's monthly comment allowance is spent and there is no stored snapshot to answer withUpgrade your plan, or wait for the next month. Posts whose comments are already stored keep listing as normal.
403insufficient_scopeThe 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 holdThe message names the missing scope. Use a key that has it, or mint one from an unrestricted key. See Scopes.
404not_foundResource doesn't exist or belongs to another userVerify the ID. Posts/accounts are scoped to your user.
404first_comment_not_foundA retry of a first comment on a post that has noneAdd one with PATCH /api/v1/posts/:id while the post is still unpublished, or send a new post with first_comment.
409post_not_editableThe post is publishing, published, failed or deleted, or the draft has already been promoted, so it can no longer be editedRead it back with GET /api/v1/posts/:id to see where it ended up.
409first_comment_not_retryableThe first comment is already posted, the post has not published yet, or another attempt is in flightRead 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.
409comment_permission_requiredThe account needs re-authorizing before Chirpie can read or manage its commentsReconnect the account from the dashboard. Posting and analytics keep working meanwhile.
409idempotency_in_progressThe same Idempotency-Key is still being worked onRetry the identical request to collect the replay. Chirpie does not queue you behind the first attempt. See Idempotency.
422idempotency_key_reusedThe same Idempotency-Key arrived with a different method, path or bodyUse a fresh key for a different request. A key is bound to the exact request it was first used with. See Idempotency.
429rate_limitedThe 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 aboveRetry after Retry-After. It is always present on this one. Pace on X-RateLimit-Remaining to stay ahead of it.
429account_limit_reachedConnecting or activating an account would exceed your plan's account limitDeactivate another account, or upgrade your plan. Waiting does not help, so this response carries no Retry-After.
429analytics_refresh_rate_limited?refresh=true on analytics inside the 5-minute floor for that postRetry after Retry-After, or drop refresh and read the stored numbers, which are always available.
429comment_sync_rate_limitedThe platform is rate limiting comment reads and there is no stored snapshot to answer withRetry after Retry-After. A post whose comments are already stored keeps listing from the snapshot.
429usage_limit_exceededOn 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 tooUpgrade your plan, or wait for the next month. Waiting a few seconds does not help, so this response carries no Retry-After.
500internal_errorUnexpected server error, including one while starting an account connectionRetry after a moment. If persistent, contact support.
501platform_coming_soonThe platform is recognised but cannot be connected yetConnect one of the available platforms for now.
501platform_unsupportedThe platform is not supportedUse one of the supported platforms.
501comments_unsupportedThe platform exposes no comment read for this postRead the comments on the platform itself. See the Comments API table.
501comment_action_unsupportedThe platform allows reading comments but not this actionCheck meta.capabilities on the listing before offering the control.
501delete_unsupportedChirpie 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.
502upstream_errorPlatform 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.
429first_comment_rate_limitedThe platform is rate limiting posts from this account, so the first comment could not go outThe post itself published and is untouched. Try the comment again shortly.
502first_comment_failedThe platform refused a retry of a first commentThe 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.
502thread_rollback_incompleteA thread failed part-way and Chirpie could not remove every post it had already publishedRead error.thread_rollback.still_live for the posts that are still up, delete them, then retry. Retrying without doing that publishes them twice.
503media_storage_failedThe media was downloaded but could not be storedTemporary 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 DELETE is 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.

{
  "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, draft
  • POST /api/v1/threads: account_id, account_ids, account_configurations, posts, first_comment, configuration, schedule_at, timezone, draft
  • PATCH /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.

PlatformCommon causesFix
XRate limit (~200 posts/day), suspended account, content policyWait, 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 accountReconnect the account from the dashboard. Text posts keep working meanwhile
BlueskyRevoked app password, expired session, rate limitGenerate a new app password and reconnect
LinkedInRate limit (~100 posts/day per member), expired tokenRe-authorize from the dashboard
ThreadsRate limit (250 posts/24h), expired Meta tokenRe-authorize from the dashboard
Threads (on DELETE)Reconnect your Threads account to delete posts.: Threads has not granted permission to delete posts on that accountReconnect the account from the dashboard. Posting, scheduling and analytics keep working meanwhile
MastodonRate limit (300 posts/3h by default), revoked tokenRe-authorize from the dashboard
InstagramRate limit (50 posts/24h), expired tokenRe-authorize from the dashboard
FacebookRate limit (4,800 calls/24h per Page), revoked Page permissionsRe-authorize from the dashboard
TelegramInvalid bot token, bot not an admin of the chat, wrong chat IDVerify 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.

MessageMeaning
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.

MessageMeaning
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.

On this page