ChirpieDocs
API Reference

Threads

Create multi-post threads that publish as connected replies on X, Bluesky, Threads, Mastodon, and Telegram, or as standalone posts on LinkedIn, Instagram, and Facebook.

Create a Thread

Threads are arrays of 2-25 posts published in order as connected replies. Each post in the thread replies to the previous one. Threads work on X, Bluesky, Threads, Mastodon, and Telegram (native reply threading). On LinkedIn, Instagram, and Facebook, each post in the thread is published as a standalone post (these platforms do not support native threading). More platforms are coming soon.

Platform threading behavior:

  • X/Twitter: Native threading via reply chains
  • Bluesky: Native threading via reply chains
  • Threads (Meta): Native threading via reply_to_id
  • Mastodon: Native threading via in_reply_to_id
  • Telegram: Native threading via reply_to_message_id
  • LinkedIn: Each post is published as a standalone post (no native threading support)
  • Instagram: Each post is published as a standalone post (no native threading support)
  • Facebook: Each post is published as a standalone post (no native threading support)

A thread is all or nothing

If any part of a thread fails to publish, every part that had already published is deleted from the platform and the whole thread's quota is refunded. That holds on every path a thread publishes by: immediately, through the scheduler, when a draft is promoted, and for each account of a multi-account publish. Where a thread degrades to standalone posts, the same rule covers the standalone posts made so far, on LinkedIn and Facebook. Instagram depends on how the account was connected: an account connected via Facebook rolls back like any other, and an account connected through Instagram cannot, so a failed thread there leaves every part it had already published live, and names them.

The error says what it removed, in error.thread_rollback, and the code tells you whether a retry is safe:

  • 502 upstream_error: still_live is empty. Nothing of the thread is on the platform, so repeating the same request is safe.
  • 502 thread_rollback_incomplete: Chirpie could not remove everything. The posts in still_live are really still up, with their ids and public URLs. Repeating the request would publish them a second time, so remove them first, or post only the remaining parts.

rollback_supported: false means Chirpie cannot delete a published post for that account, so nothing could be removed. On Instagram that is the case for an account connected through Instagram: such a thread leaves every part it had already published live, and says which. An Instagram account connected via Facebook rolls back. See the delete table.

A scheduled thread that fails is retried three times before Chirpie gives up. The one exception is a rollback that left posts live: that thread fails at once, because a retry would publish them twice, and the reason names them.

The full response shape, with examples: A thread that failed part-way.

POST /api/v1/threads
Authorization: Bearer chirpie_sk_YOUR_KEY
Idempotency-Key: 8f1c0e0c-6d51-4d73-9f4e-6b2a0a2d3f11
Content-Type: application/json

Headers

HeaderRequiredDescription
AuthorizationYesBearer chirpie_sk_.... The key needs the posts:write scope
Content-TypeYesapplication/json
Idempotency-KeyNoMakes a retry of this exact request safe for 24 hours. The same key with the same body replays the original response with Idempotent-Replay: true instead of publishing the thread again. Max 255 characters. See Idempotency

A thread is the request most worth sending a key with: it is the longest-running call in the API, so it is the one most likely to time out on your side after Chirpie has already started publishing.

Request Body

FieldTypeRequiredDescription
account_idstring (UUID)Yes, unless account_ids is givenID of the connected account. Note: LinkedIn, Instagram, and Facebook degrade to standalone posts.
account_idsstring[] (UUID)Yes, unless account_id is given1 to 25 connected accounts to publish the thread to in one request, given instead of account_id. Duplicates are refused. Naming it selects the fan-out response shape, even for a single account. See Post to several accounts at once
account_configurationsobjectNoPer-account overrides, keyed by account id. Only with account_ids, and every key must be in it. Each value takes posts, which replaces the whole array for that account (still 2-25 parts, or 1-25 when draft is true), first_comment, which replaces the shared one for that account ("" publishes that account with none), and configuration, which replaces the whole publishing-options block for that account. See Post to several accounts at once
postsarrayYesArray of 2-25 post objects, or 1-25 when draft is true
posts[].textstringYes, unless draft is truePost content. Max length depends on platform: 280 (X standard), 25,000 (X Premium), 300 (Bluesky), 3,000 (LinkedIn), 500 (Threads), 500 (Mastodon), 2,200 (Instagram), 63,206 (Facebook), or 4,096 (Telegram, 1,024 when the post carries media)
posts[].mediaobject[]NoUploaded files and public links for this post, each with optional alt text. See Media
posts[].media_idsstring[]NoIds from an upload, when no alt text is needed
posts[].media_urlsstring[]NoPublic image/video URLs per post. X: up to 4 images or 1 video. Bluesky: up to 4 images (977 KB each), no video. LinkedIn: up to 4 images (JPEG/PNG/GIF, 8 MB max), no video. Threads: 1 image, no video. Mastodon: up to 4 images or 1 video. Instagram: requires images, up to 10 (carousel), no video. Facebook: up to 10 images, no video. Telegram: up to 10 images, or 1 video. Anything the platform cannot take is refused with 400 unsupported_media, never dropped.
first_commentstringNoOne comment to publish under the thread the moment it goes out, under the last part. X, Threads, Instagram and Facebook only. It counts as one post against your monthly quota. See First comment on a thread
configurationobjectNoPer-platform publishing options for every part of the thread, keyed by platform. A thread publishes to the feed, so an Instagram story or reel, and a Facebook Page story, are refused: each is a single post. See Publishing options on a thread
schedule_atstring (ISO 8601)NoSchedule the entire thread. Either an absolute instant carrying a timezone (...Z, +02:00, -05:00), normalized to UTC, or a local time with no offset (2026-11-01T09:30:00), read in timezone or the timezone saved on your account. A local time with neither, or a relative offset like +30m, is rejected. On a draft thread it is only the time you have in mind.
timezonestring (IANA)NoThe timezone a schedule_at with no offset is read in, such as America/New_York. Daylight saving is worked out for the date you named. Ignored when schedule_at already carries an offset, but still validated. A fixed offset like +02:00 is not accepted here. See Timezones
draftbooleanNoSave the thread without sending it. Every part is stored as a draft, nothing reaches the platform, and nothing counts against your quota. See Draft threads

Only the fields above are accepted. Any other top-level field returns 400 bad_request rather than being ignored. In particular the request field is schedule_at. Sending scheduled_at (the field name in the response) is rejected with Unknown field 'scheduled_at'. Did you mean 'schedule_at'?. If you are round-tripping a post object you read back from the API, send only the fields listed above. group_id, which every returned thread and post carries, is one of the fields that cannot be sent back: it is set by naming account_ids, never by hand.

First comment on a thread

first_comment on a thread is one comment for the whole thread, published under the last part, and reported on that part's first_comment. It is the same field, and the same rules, as a first comment on a post: X, Threads, Instagram and Facebook only, refused with 400 first_comment_unsupported anywhere else, and counted as one post against your monthly quota.

curl -X POST https://chirpie.ai/api/v1/threads \
  -H "Authorization: Bearer chirpie_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "account_id": "550e8400-e29b-41d4-a716-446655440000",
    "posts": [
      { "text": "We rebuilt scheduling this week." },
      { "text": "Queued posts now publish within five minutes of their time." }
    ],
    "first_comment": "Full write-up: https://example.com/blog/scheduling"
  }'

Every other part comes back with first_comment: null. A failed first comment never fails its thread: the thread publishes, the last part carries first_comment.status: "failed" with a reason, and POST /api/v1/posts/:id/first-comment on that part sends it again.

Publishing options on a thread

configuration works on a thread exactly as it does on a post, with one extra rule: a thread publishes to the feed. An Instagram story or reel, and a Facebook Page story, are each a single post, so a thread naming either placement is refused with 400 configuration_unsupported telling you to send it with POST /api/v1/posts instead. Everything else a block carries (an Instagram collaborator, a user tag, a Facebook link) applies to every part alike.

Publishing to several accounts

Name account_ids instead of account_id and one request publishes the thread to up to 25 accounts. An account configuration takes posts, which replaces the whole array for that account:

curl -X POST https://chirpie.ai/api/v1/threads \
  -H "Authorization: Bearer chirpie_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "account_ids": ["ACCOUNT_A", "ACCOUNT_B"],
    "posts": [{ "text": "one" }, { "text": "two" }],
    "account_configurations": {
      "ACCOUNT_B": { "posts": [{ "text": "a" }, { "text": "b" }, { "text": "c" }] }
    }
  }'

The response is a group_id plus one result per account, in the order the accounts were named, 201 when every account succeeded and 207 Multi-Status when at least one did not. Each result entry carries thread_id and thread where a post fan-out carries post_id and post, and platform_post_url is the first part's permalink.

{
  "data": {
    "group_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "results": [
      {
        "account_id": "ACCOUNT_A",
        "platform": "x",
        "success": true,
        "thread_id": "thread-uuid-here",
        "platform_post_url": "https://x.com/chirpie_ai/status/1234567890",
        "status": "published",
        "thread": { "id": "thread-uuid-here" },
        "error": null
      },
      {
        "account_id": "ACCOUNT_B",
        "platform": "bluesky",
        "success": false,
        "thread_id": null,
        "platform_post_url": null,
        "status": null,
        "thread": null,
        "error": { "code": "upstream_error", "message": "Bluesky API error: ..." }
      }
    ]
  }
}

Full detail, including what is refused whole versus per account and how quota is reserved and refunded: Post to several accounts at once.

Example

curl -X POST https://chirpie.ai/api/v1/threads \
  -H "Authorization: Bearer chirpie_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "account_id": "550e8400-e29b-41d4-a716-446655440000",
    "posts": [
      { "text": "I just shipped a feature using Chirpie. Here'\''s how it went 🧵" },
      { "text": "First, I connected my X account via OAuth. Took about 30 seconds." },
      { "text": "Then I created an API key and made my first POST request. Done." },
      { "text": "The whole thing took under 2 minutes. Try it: chirpie.ai" }
    ]
  }'

Response 201 Created

{
  "data": {
    "id": "thread-uuid-here",
    "platform": "x",
    "account_id": "550e8400-e29b-41d4-a716-446655440000",
    "posts": [
      {
        "id": "post-1-uuid",
        "text": "I just shipped a feature using Chirpie...",
        "platform_post_id": "1234567890",
        "platform_post_url": "https://x.com/chirpie_ai/status/1234567890",
        "status": "published",
        "thread_order": 0,
        "created_at": "2026-03-23T10:00:00.000Z"
      },
      {
        "id": "post-2-uuid",
        "text": "First, I connected my X account...",
        "platform_post_id": "1234567891",
        "platform_post_url": "https://x.com/chirpie_ai/status/1234567891",
        "status": "published",
        "thread_order": 1,
        "created_at": "2026-03-23T10:00:00.000Z"
      }
    ],
    "status": "published",
    "scheduled_at": null,
    "group_id": null,
    "created_at": "2026-03-23T10:00:00.000Z"
  }
}

group_id is the fan-out this thread belongs to when it was created by naming account_ids, and null when it was created with a single account_id. Pass it to GET /api/v1/posts?group_id=... to read every post of the fan-out back.

Each post in the response carries platform_post_url, its own public link on the platform, every part of the thread included. See platform_post_url for the link each platform gives and the few cases where it is null.

Billing

A thread counts as N posts where N is the number of posts in the thread. Both post and scheduled limits are checked against the thread size.

A draft thread counts as zero. Nothing is charged while it sits saved, and the whole thread's quota is taken at the moment it is scheduled or published.

A first comment is a post on the platform too, so a thread that carries one counts as N + 1.

Error Responses

StatusCodeCondition
400bad_requestLess than 2 or more than 25 posts (1 to 25 when draft is true), invalid text, a missing required field (posts[0].text is required, which a draft may leave out), a schedule_at that is a local time with no timezone on the request and none saved on the account, a relative offset, or schedule_at in the past (a draft's remembered time may be any time)
400bad_requestOn a multi-account request: more than 25 ids, a duplicate id in account_ids, an account_configurations key that is not in account_ids, or an override posts array outside 2-25 parts (1-25 when draft is true). Nothing is published
401unauthorizedInvalid or missing API key
400first_comment_unsupportedfirst_comment was sent to an account whose platform has no first comment. The message names the platform and the four that take one. On a fan-out, give that account "first_comment": "". Nothing is published
400configuration_unsupportedA configuration block on a platform that takes none, on a platform no account in the request uses, a field the placement does not carry, or an Instagram story or reel or a Facebook Page story, each of which is a single post. Nothing is published
402x_link_posts_require_paid_planA post in the thread contains a link, the account is X, and the plan is Free
404not_foundAccount not found or inactive
403insufficient_scopeThe API key does not hold posts:write. See Scopes
409idempotency_in_progressAn identical request with the same Idempotency-Key is still running. Retry for the replay
422idempotency_key_reusedThe same Idempotency-Key was used for a different request
429rate_limitedThe burst limit on your API key, which is your plan's ceiling. Carries Retry-After. See Rate limits
429usage_limit_exceededThe thread would cross your monthly post quota (Free plan) or your scheduled-post quota. The message names the arithmetic. Waiting does not help, so there is no Retry-After
502upstream_errorPlatform API error during publishing. The thread is all or nothing: every post already published is deleted from the platform, no rows are kept, and the whole thread's quota is refunded. error.thread_rollback names what was removed
502thread_rollback_incompleteThe same, except Chirpie could not remove every post it had already published. error.thread_rollback.still_live names the ones that are still up, with their ids and URLs. Do not retry blindly: that would publish them twice

On a multi-account request, a validation failure names the account. Every account's thread is checked before anything publishes, so a part over one platform's limit, unsupported media, a first_comment on an account that cannot take one (give that account "first_comment": ""), a publishing option a thread cannot carry, or an X link post on Free refuses the whole request with its usual status and code, and the message is prefixed Account <id>: . Nothing is published and no quota is taken. Quota is reserved once for the group, one unit per part per account, and an account whose thread fails to publish gives back exactly its own share. A first comment is one unit more, per account that actually sends one, taken when the comment goes out rather than with the group's reservation, and given back if it does not land. A platform failure for one account comes back as 207 Multi-Status with that account's results[].error set, and the other accounts stay published.

X link posts: Each post in an X thread whose text contains a link is billed at $0.25 on paid plans, on top of your monthly allowance, because X charges API operators $0.20 per post containing a URL. On the Free plan the whole thread is rejected with 402 x_link_posts_require_paid_plan before any quota is consumed.

Exempt: X accounts connected with your own X API credentials, and every other platform.

Scale and Enterprise run on custom quotas rather than per-post overage. Talk to us at hey@chirpie.ai.

Scheduled Threads

When schedule_at is provided, all posts in the thread are stored with status: "scheduled". The entire thread is published atomically by the scheduler, so if any post fails, the whole thread is rescheduled for retry.

Draft threads

Send draft: true and the thread is saved without going anywhere. Every part is stored with status: "draft", nothing reaches the platform, and nothing counts against your quota until the thread is promoted.

A draft thread is held to far less than a real one: it may be a single part long while you are still writing it, and a part may have no text yet. The response carries a warnings array saying, per account, what would stop it publishing as it stands, including whether the platform will publish the parts as standalone posts rather than a reply chain.

curl -X POST https://chirpie.ai/api/v1/threads \
  -H "Authorization: Bearer chirpie_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "account_id": "550e8400-e29b-41d4-a716-446655440000",
    "posts": [{ "text": "Opening line, rest to come" }],
    "draft": true
  }'

Promote it with PATCH /api/v1/posts/:id on any of its parts: schedule_at queues the whole thread, publish: true sends all of it now, and the response names every part it promoted in promoted_from_draft_ids. Promotion runs every rule a create runs, the two-post minimum included. Deleting any part of a draft thread deletes the whole thread, and the response lists the ids in deleted_ids, and a draft thread saved to several accounts goes in one call too. Full detail: Drafts.

Cancelling a Scheduled Thread

A scheduled thread is cancelled as a unit, exactly as it is published as one. Cancelling one post in a scheduled thread cancels the thread. DELETE /api/v1/posts/:id on any post of the thread cancels every post in it, and returns their ids:

curl -X DELETE https://chirpie.ai/api/v1/posts/b2c3d4e5-... \
  -H "Authorization: Bearer chirpie_sk_YOUR_KEY"
{
  "data": {
    "id": "b2c3d4e5-...",
    "deleted": true,
    "cancelled_ids": ["a1b2c3d4-...", "b2c3d4e5-...", "c3d4e5f6-..."]
  }
}

Nothing is billed for a thread that never published, and the whole thread's monthly quota is returned. To change one post of a scheduled thread, cancel the thread and schedule a new one.

On this page