ChirpieDocs
API Reference

Posts

Create, save as a draft, list, retrieve, edit, and delete posts via the Chirpie API, and publish a first comment under a post.

Create a Post

Posts are published immediately by default. Add schedule_at to schedule for later.

POST /api/v1/posts
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 again. Max 255 characters. See Idempotency

Request Body

FieldTypeRequiredDescription
account_idstring (UUID)Yes, unless account_ids is givenID of the connected account (X, Bluesky, LinkedIn, Threads, Mastodon, Instagram, Facebook, or Telegram)
account_idsstring[] (UUID)Yes, unless account_id is given1 to 25 connected accounts to publish to in one request, given instead of account_id. Duplicates are refused. Naming it selects the fan-out response shape below, 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 text, first_comment ("" publishes that account with none), configuration (replacing the whole block for that account), and one of media, media_ids, media_urls. See Post to several accounts at once
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)
mediaobject[]NoUploaded files and public links, each with optional alt text. An entry carries either id (from an upload) or url. See Media
media_idsstring[]NoIds from an upload, when no alt text is needed
media_urlsstring[]NoPublic image/video URLs (up to 4 on most platforms, 10 on Instagram, Facebook and Telegram, 1 on Threads; see Media below)
first_commentstringNoA comment to publish under the post the moment it goes out. X, Threads, Instagram and Facebook only. It counts as one post against your monthly quota. See First comment
configurationobjectNoPer-platform publishing options, keyed by platform. Instagram takes a feed post, a story or a reel; a Facebook Page takes a feed post or a story. Leave it out and everything publishes to the feed. See Publishing options
schedule_atstring (ISO 8601)NoSchedule for a future time. Either an absolute instant carrying a timezone (2027-04-01T14:00:00Z, 2027-04-01T16:00:00+02:00, 2027-04-01T09:00: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 it is only the time you have in mind, so it may be any time at all
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 post without sending it. Nothing reaches the platform, nothing is queued, and nothing counts against your quota. See Drafts

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 post carries, is one of the fields that cannot be sent back: it is set by naming account_ids, never by hand.

Publishing to several accounts

Name account_ids instead of account_id and one request publishes to up to 25 accounts, each with optional per-account text and media in account_configurations. The response is a group_id plus one result per account, 201 when every account succeeded and 207 Multi-Status when at least one did not.

curl -X POST https://chirpie.ai/api/v1/posts \
  -H "Authorization: Bearer chirpie_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "account_ids": ["ACCOUNT_A", "ACCOUNT_B"],
    "text": "Shipping new features today!",
    "account_configurations": {
      "ACCOUNT_B": { "text": "Shipping new features today. Write-up on the blog." }
    }
  }'
{
  "data": {
    "group_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "results": [
      {
        "account_id": "ACCOUNT_A",
        "platform": "x",
        "success": true,
        "post_id": "a1b2c3d4-...",
        "platform_post_url": "https://x.com/chirpie_ai/status/1234567890",
        "status": "published",
        "post": { "id": "a1b2c3d4-..." },
        "error": null
      },
      {
        "account_id": "ACCOUNT_B",
        "platform": "bluesky",
        "success": false,
        "post_id": null,
        "platform_post_url": null,
        "status": null,
        "post": null,
        "error": { "code": "upstream_error", "message": "Bluesky API error: ..." }
      }
    ]
  }
}

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

First comment

first_comment publishes one comment under the post the moment the post goes out: the "link in the first comment" pattern. The comment is posted by the same account as a reply to the post, is recorded as one of your own comments, and appears in the post's comment thread with own: true.

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

Which platforms take one

PlatformFirst comment
X/TwitterYes
ThreadsYes
InstagramYes
FacebookYes
Bluesky, LinkedIn, Mastodon, TelegramNo

Sending first_comment to any other platform is refused with 400 first_comment_unsupported, naming the platform that refused and the four that take one. It is never silently dropped.

Length, and what it costs

The character limit is the account's own post limit: 280 on standard X, 25,000 on X Premium, 500 on Threads, 2,200 on Instagram, 63,206 on Facebook.

A first comment is a post on the platform, so it counts as one post against your monthly quota, exactly as a comment reply does. On X, a first comment whose text contains a link carries the same link-post charge a link post does: $0.25 on paid plans, and on the Free plan the request is rejected with 402 x_link_posts_require_paid_plan before the post is published, so nothing goes out. X accounts connected with your own X API credentials are exempt.

Where else it can be sent

  • POST /api/v1/threads: one comment for the whole thread, published under the last part and reported on that part.
  • Fan-out requests: a top-level first_comment applies to every account. An entry in account_configurations may replace it with its own, or set it to "" so that account publishes with no first comment, which is how one request reaches an account whose platform has none. See Post to several accounts at once.
  • Drafts: stored with the draft and carried into the post when the draft is promoted. See warnings.
  • PATCH /api/v1/posts/:id: replaces the first comment, and the empty string "" removes it.
  • Scheduled posts: the first comment stays pending until the post publishes. Both go out at the scheduled time, the post first.

What comes back

Every post payload carries first_comment: null when none was asked for, and otherwise an object.

{
  "first_comment": {
    "text": "Full write-up: https://example.com",
    "status": "posted",
    "comment_id": "6f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
    "error": null
  }
}
FieldMeaning
textThe comment as it will be, or was, published
statuspending, posted or failed. pending means the post has not published yet, or the comment is on its way
comment_idThe comment's id in the post's comment thread, so it can be read, hidden or deleted through the Comments API like any other comment. Null before the comment is posted, and, rarely, on a posted comment Chirpie could not record: the comment is live on the platform, and the next comment refresh picks it up
errorA worded reason when status is failed, and null otherwise

A failed first comment never fails its post. The post publishes as normal, first_comment.status is failed, and error says why. Send it again with POST /api/v1/posts/:id/first-comment.

Publishing options

configuration says how a platform should publish this post, keyed by platform slug. One post can mean different things on different platforms: an Instagram account takes the same picture as a feed post, a story or a reel, and a Facebook Page takes it as a feed post or a story. Every other platform publishes one way and takes no configuration block.

{
  "account_id": "550e8400-e29b-41d4-a716-446655440000",
  "text": "Behind the scenes of this week.",
  "media_urls": ["https://example.com/clip.mp4"],
  "configuration": {
    "instagram": { "placement": "reel", "share_to_feed": true }
  }
}
PlatformPlacementsOptions
Instagramfeed (default), story, reelcollaborators, user_tags, cover, video_cover_timestamp_ms, share_to_feed, trial_reel
Facebookfeed (default), storylink

Each platform's placements, the options each one carries, the media rules per placement and what can and cannot be deleted: Instagram publishing options and Facebook publishing options.

Four rules apply to every block:

  • Nothing is dropped quietly. A block keyed on a platform that takes no options, on a platform no account in the request uses, or a field the chosen placement does not carry, is refused with 400 configuration_unsupported naming the platform and the field. Nothing is published.
  • One block per platform, not per account. A top-level configuration applies to every account of that platform in the request. An entry in account_configurations replaces the whole block for one account, rather than merging into it.
  • A story or a reel is a single post. A thread asking for either placement is refused; send it with POST /api/v1/posts.
  • A story carries no caption and no first comment. Send the post with empty text, and leave first_comment out.

The placement decides the media rules too, so a story is held to a story's limits rather than the feed's. On a PATCH, configuration: {} puts a post back to a plain feed post.

Media Support

Pass public URLs in media_urls, or upload the files first and name them in media. Chirpie downloads, validates, and uploads them to the target platform automatically. Use one of media, media_ids and media_urls, not several: sending two is refused with 400 bad_request. Full detail, including alt text and the upload endpoint: Media.

PlatformImagesMax Image SizeMax ImagesVideoMax Video Size
X/TwitterJPEG, PNG, WebP, GIF5 MB4MP4, MOV512 MB (1 per post)
BlueskyJPEG, PNG, WebP, GIF977 KB4Not supportedn/a
LinkedInJPEG, PNG, GIF8 MB4Not supportedn/a
ThreadsJPEG, PNG, WebP, GIF5 MB1Not supportedn/a
MastodonJPEG, PNG, WebP, GIF8 MB4MP4, MOV40 MB (1 per post)
InstagramJPEG, PNG8 MB10 (carousel)Not supportedn/a
FacebookJPEG, PNG, GIF10 MB10Not supportedn/a
TelegramJPEG, PNG, GIF, WebP5 MB10MP420 MB (1 per post)

More platforms coming soon.

A Mastodon video may take a moment. Mastodon transcodes video after it is uploaded, so Chirpie waits for the instance to finish before publishing the status. That can add a few seconds to the request. If the instance is still working after a minute, the call returns 502 upstream_error saying the instance is still processing the video, and nothing is published. Send the same request again. Images publish immediately.

Media is never dropped silently. Anything the target platform cannot accept is refused with 400 unsupported_media and nothing is posted, so a post either carries every item in media_urls or does not publish at all. Sending a video to Bluesky, LinkedIn, Threads, Instagram or Facebook comes back as Bluesky does not support video. Remove the video or post it to X, Mastodon or Telegram. The same applies to a format the platform cannot decode (WebP on LinkedIn) and to a file over its size limit.

Describe your media. Every entry in media takes an alt string, which is sent to X, Bluesky, LinkedIn, Mastodon, Instagram and Facebook. Leave it out and no description is sent: nothing is invented for you. See Media for the per-platform limits.

Other media requirements: a video is always posted on its own, never alongside images. Threads takes 1 image per post. Instagram requires media on every post, so text-only posts are not supported: a feed post takes up to 10 images as a carousel, and a story takes one image or one video, and a reel takes one video. Facebook supports Pages only (not personal profiles).

X media needs its own permission. If an X post with media_urls comes back with Reconnect your X account to upload media., X has not granted Chirpie permission to upload on that account. Reconnect it from Accounts and the post will go through. Text posts are unaffected either way.

For scheduled posts, media is downloaded, validated and stored immediately, since the URLs may expire before publish time. That means an unusable file is reported when you schedule the post, not silently at publish time. The stored media is uploaded to the platform when the post publishes and cleaned up afterwards.

Example

curl -X POST https://chirpie.ai/api/v1/posts \
  -H "Authorization: Bearer chirpie_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "account_id": "550e8400-e29b-41d4-a716-446655440000",
    "text": "Shipping new features today!",
    "media_urls": ["https://example.com/screenshot.png"],
    "schedule_at": "2026-04-01T14:00:00Z"
  }'

Response 201 Created

{
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "platform": "x",
    "account_id": "550e8400-e29b-41d4-a716-446655440000",
    "text": "Shipping new features today!",
    "media_urls": null,
    "first_comment": null,
    "platform_post_id": null,
    "platform_post_url": null,
    "status": "scheduled",
    "scheduled_at": "2026-04-01T14:00:00.000Z",
    "published_at": null,
    "thread_id": null,
    "thread_order": null,
    "group_id": null,
    "error_message": null,
    "retry_count": 0,
    "created_at": "2026-03-23T10:00:00.000Z"
  }
}

group_id on a returned post

Every post object carries group_id: the fan-out it belongs to when it was created by naming account_ids, and null when it was created with a single account_id. Pass it to List Posts to read every post of the same fan-out back. Thread objects carry it too.

account_id on a returned post

Every post object carries account_id, the account it went out through. It is null when that account has since been disconnected: disconnecting removes the connection but keeps everything the account published, so an old post outlives the account it belongs to. platform still says where the post went.

On a request body account_id is always required; only a returned post can carry null.

platform_post_url

Every post object (create, get, and list) carries platform_post_url: the public link to the post on the platform. It is null until the post is published.

PlatformLink
Xhttps://x.com/<username>/status/<id>
Blueskyhttps://bsky.app/profile/<handle>/post/<rkey>
Mastodon<instance>/@<user>/<id>
LinkedInhttps://www.linkedin.com/feed/update/<urn>/
Facebookhttps://www.facebook.com/<pageId>_<postId>
Telegramhttps://t.me/<channel>/<id>, for public @channel chats only
ThreadsThe link Threads gives for the post, such as https://www.threads.com/@<username>/post/<code>
InstagramThe link Instagram gives for the post, such as https://www.instagram.com/p/<code>/ or /reel/<code>/

Most platforms' links are built from platform_post_id. Threads and Instagram answer a publish with an internal media id that no public link can be built from, so Chirpie asks the platform for the post's own link the moment it publishes and keeps it. That covers every part of a thread, carousels, and reels.

Two cases can still leave it null on Threads or Instagram:

  • The platform gave no link. Instagram may return none for a story, and omits it for media it has flagged for copyrighted material. A story is only visible for 24 hours in any case.
  • The link could not be read when the post went out. The post is published all the same, since a missing link never fails a post. The next GET /api/v1/posts/{id} asks the platform once more and keeps the answer, so from then on every listing carries it too. Listings never ask the platform themselves.

A Telegram message in a private chat or a group with no public @name has no public link, and stays null.

error_message and retry_count

Every post object carries both, so a post that is not where you expected it can say why without a second request.

Value of error_messageWhat it means
nullNothing went wrong
Any other sentence, with status: "failed"The platform's own refusal, after Chirpie ran out of retries
"account_disconnected"Cancelled because the account was disconnected
"account_deactivated"Cancelled because the account was switched off
"connection_lost"Cancelled because the platform stopped accepting the account's credential

The last three come with status: "deleted": Chirpie cancelled the post rather than you deleting it, and the monthly quota it had reserved was returned. The account's inactive_reason says the same thing from the other side. See When a connection dies.

retry_count is how many publish attempts have been made. Chirpie retries a failing scheduled post three times before giving up, so a post still scheduled with a non-zero retry_count has already missed at least one attempt and is waiting to try again.

Comments on a post

Once a post is published, the comments it receives are available through the Comments API, keyed on the post's id: GET /api/v1/posts/:id/comments. From there you can reply to a comment, hide it, or delete it, on the platforms that allow each. A reply counts as one post against your monthly quota.

A first comment is one of those comments: it is listed with the rest, carries own: true, and its comment_id is the id you hide or delete it by.

Error Responses

StatusCodeCondition
400bad_requestA missing required field (account_id and text are required), an unknown top-level field, text over the account's limit, too many media_urls for the platform, a schedule_at with no timezone anywhere to read it in, a timezone that is not an IANA name, a relative offset, a past time, or a time within 5 minutes of another scheduled post on the same account
400unsupported_mediaA media_urls item the platform cannot accept: a video where it takes images only, a format it cannot decode, or a file over its size limit. Nothing is posted
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 posted
400configuration_unsupportedA configuration block on a platform that takes none, on a platform no account in the request uses, or a field the chosen placement does not carry. Also a story or a reel with text or a first_comment. The message names the field and what to do. Nothing is posted
400bad_requestOn a multi-account request: more than 25 ids, a duplicate id in account_ids, or an account_configurations key that is not in account_ids. Nothing is posted
400bad_requestA group_id on List Posts that is not a UUID
401unauthorizedInvalid or missing API key
402x_link_posts_require_paid_planX post contains a link and the account is on the Free plan
404not_foundAccount not found or inactive
403insufficient_scopeThe API key does not hold posts:write, or posts:read on a read. 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_exceededYour monthly post quota, or your monthly scheduled-post quota, is spent. Waiting does not help, so there is no Retry-After
502upstream_errorPlatform API returned an error

On a multi-account request, a validation failure names the account. Every account's body is checked before anything publishes, so a text 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 placement does not 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. Only the platform call itself fails per account, and that comes back as 207 Multi-Status with the failure in results[].error.

X link posts: X charges API operators $0.20 for every post containing a URL. On paid plans, an X post whose text contains a link (an http(s):// URL or a bare domain X would auto-link, such as chirpie.ai) is billed at $0.25, on top of your monthly allowance. On the Free plan the request is rejected with 402 x_link_posts_require_paid_plan.

Exempt: X accounts connected with your own X API credentials carry no surcharge, and you pay X directly at your own rate. Posts to every other connected platform are never surcharged.

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


Drafts

Send draft: true on a create call and Chirpie saves the post and does nothing else. Nothing reaches the platform, nothing is queued, and nothing is charged: a draft counts against neither your monthly post quota nor your scheduled post quota until you promote it.

Because a draft is work in progress, the usual checks are relaxed. text may be empty or left out, media may be missing even on a platform that normally requires it, a draft thread may be a single part where a real thread needs at least two, and schedule_at is only the time you have in mind: it is remembered, it is not a promise to publish, and it may already have passed.

What is still refused outright, with the same codes as always: an account id that is not yours or is not active (404 not_found), a media id you do not hold (404 media_not_found), an unknown top-level field, text over 25,000 characters, more than 25 ids in account_ids, a duplicate id, and two media spellings in one request (the rest are 400 bad_request).

Save a draft

curl -X POST https://chirpie.ai/api/v1/posts \
  -H "Authorization: Bearer chirpie_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "account_id": "550e8400-e29b-41d4-a716-446655440000",
    "text": "Half an idea. Finish it later.",
    "draft": true
  }'

The post comes back with status: "draft". account_ids works the same way, and the answer is then the fan-out shape, a group_id plus one result per account, with success: true and status: "draft" on every entry. POST /api/v1/threads takes draft too.

warnings

Every draft response carries a warnings array, and an empty array is the useful answer "nothing would go wrong". Each entry says, for one account, what would happen if the draft were sent as it stands, so you find out while you are still writing rather than at publish time.

{
  "data": {
    "id": "a1b2c3d4-...",
    "platform": "bluesky",
    "account_id": "550e8400-...",
    "text": "A long first attempt...",
    "status": "draft",
    "scheduled_at": null,
    "published_at": null,
    "warnings": [
      {
        "account_id": "550e8400-...",
        "platform": "bluesky",
        "code": "bad_request",
        "message": "Text must be 300 characters or less (Bluesky account). Got 400."
      }
    ]
  }
}

Anything that would really be refused carries the same code and the same sentence the API would answer the send with, so there is one vocabulary to read rather than two: bad_request, unsupported_media, first_comment_unsupported, configuration_unsupported, x_link_posts_require_paid_plan. A draft carries its first_comment and its configuration too, into the post it becomes when it is promoted, and a draft whose account cannot take one comes back with a first_comment_unsupported warning rather than an error. Three codes describe a change rather than a refusal:

CodeWhat it means
thread_not_nativeThe platform has no reply chain, so each part publishes as its own standalone post
x_link_post_billedThe post contains a link, so X bills it on top of your monthly quota
schedule_at_in_pastThe time the draft remembers has already passed. Pick a new one before scheduling it

A warning never stops the save. The draft is stored either way.

Finish and promote a draft

A draft is edited with PATCH /api/v1/posts/:id like any other post that has not gone out, and an edit of a draft is relaxed in the same way: the text may be cleared, and anything that would be refused comes back on warnings rather than as an error. An edit that leaves the post a draft answers with the draft and its warnings.

Two fields promote it, and promotion is always explicit:

# Queue it for a time
curl -X PATCH https://chirpie.ai/api/v1/posts/DRAFT_ID \
  -H "Authorization: Bearer chirpie_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"schedule_at": "2027-04-01T14:00:00Z"}'

# Send it now
curl -X PATCH https://chirpie.ai/api/v1/posts/DRAFT_ID \
  -H "Authorization: Bearer chirpie_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"publish": true}'

publish: true and schedule_at are never valid together and answer 400 bad_request. To change the time a draft remembers without promoting it, send draft: true alongside schedule_at. draft: false is refused with a 400 saying to send schedule_at or publish: true instead.

Promotion runs every rule a create runs, the character limits, the media rules, the X link surcharge, the 5 minute spacing between scheduled posts and the two-post minimum for a thread, and spends the quota the draft never spent. If any of that refuses, the draft is left exactly where it was, unchanged.

The answer is the new post, carrying promoted_from_draft_id, or promoted_from_draft_ids on a draft thread, which names every part that was promoted. A promoted draft becomes a new post with a new id, and the draft itself is gone. A draft thread is promoted whole: promoting any part queues or publishes all of it.

On a post that is not a draft, publish and draft are both 400. A draft somebody else already promoted answers 409 post_not_editable, the same as a post that has already gone out.

List and delete drafts

curl "https://chirpie.ai/api/v1/posts?status=draft" \
  -H "Authorization: Bearer chirpie_sk_YOUR_KEY"

Ask for them by name. A listing with neither status nor group_id is the record of what has been sent, queued or tried, so drafts are left out of it. ?status=draft is how you read them back, and ?group_id=… returns everything one request created, drafts included.

Drafts never enter the scheduler's queue and never publish on their own. DELETE /api/v1/posts/:id on a draft removes it and refunds nothing, because nothing was charged. A draft is deleted as the one thing you saved: every account it was addressed to and every part of it go together, and the response always lists the ids in deleted_ids, however many there were.

From the dashboard

The compose dialog has a Save draft button in both Post now and Schedule mode. Saved drafts live on the Drafts tab of the Schedule page, newest first, each card showing its accounts, its text, how many parts and how many media items it carries, and the time it remembers. Open puts it all back in the compose dialog, where you can save it again, schedule it, or post it.


List Posts

GET /api/v1/posts
Authorization: Bearer chirpie_sk_YOUR_KEY

Query Parameters

ParameterTypeDefaultDescription
limitinteger20Results per page (max 100)
offsetinteger0Pagination offset
statusstringNoneFilter: draft, scheduled, publishing, published, failed, deleted. With neither this nor group_id, drafts are left out: ask for them with status=draft
account_idstringNoneFilter by account UUID
group_idstringNoneReturn every post of one fan-out, by the group_id its create call returned. Must be a UUID; anything else is 400 bad_request. See Post to several accounts at once
include_hiddenbooleanfalseInclude the posts you have hidden. Off by default on every filter, including status and group_id. See Hide a post

status=draft returns the posts you have saved but not sent. See Drafts.

status=deleted returns the posts you deleted. Deleting a post never removes it from Chirpie: the row keeps its id, its text and its history, and only its status changes. It stays in an unfiltered listing, where the dashboard shows it as "Deleted". To stop seeing a post without touching the platform, hide it.

Example

curl "https://chirpie.ai/api/v1/posts?status=published&limit=10" \
  -H "Authorization: Bearer chirpie_sk_YOUR_KEY"

Response 200 OK

data is a plain array of post objects, newest first, in the same shape as a create response. There is no total count or cursor: page with limit and offset, and stop when a page comes back shorter than limit.

{
  "data": [
    {
      "id": "a1b2c3d4-...",
      "text": "Hello world!",
      "status": "published",
      "platform_post_id": "1234567890",
      "platform_post_url": "https://x.com/chirpie_ai/status/1234567890",
      "published_at": "2026-03-23T10:01:00.000Z"
    }
  ]
}

Get a Post

GET /api/v1/posts/:id
Authorization: Bearer chirpie_sk_YOUR_KEY

Response 200 OK

Returns a single post object (same shape as list items).

StatusCodeCondition
404not_foundPost doesn't exist or belongs to another user

Update a Post

Edit a post that has not gone out yet: its text, its media, or the time it publishes. The same endpoint finishes a draft, and promotes it when you ask it to: schedule_at queues the draft, publish: true sends it now.

PATCH /api/v1/posts/:id
Authorization: Bearer chirpie_sk_YOUR_KEY
Content-Type: application/json

Request Body

Every field is optional, and only what you send changes.

FieldTypeDescription
textstringReplacement text. The same per-platform character limits apply as on create
mediaobject[]Replacement media, following the same per-platform rules as on create, with optional alt text. Send [] to remove the media
media_idsstring[]Replacement media by upload id. Send [] to remove the media
media_urlsstring[]Replacement media URLs, following the same per-platform rules as on create. Send [] to remove the media
first_commentstringA new first comment, replacing the one the post carries. Send "" to remove it. This is also how a first comment is added to a post that has not published yet. On a thread it belongs to the thread, so it applies whichever part you addressed
configurationobjectNew publishing options, replacing the ones the post carries. The media is checked again against the new placement, so a post becoming a reel needs a video attached. Send {} to put the post back to a plain feed post
schedule_atstringA new ISO 8601 publish time, in the future. Either an absolute instant carrying a timezone, or a local time with no offset read in timezone or the timezone saved on your account. On a draft it promotes: the draft becomes a scheduled post at that time, unless draft: true is sent alongside it
timezonestring (IANA)The timezone a schedule_at with no offset is read in, such as America/New_York. See Timezones
draftbooleanOnly on a draft, and only true: keep it a draft, so a schedule_at changes the time it remembers instead of queueing it
publishbooleanOnly on a draft: promote it and publish it now. Never valid together with schedule_at

Leaving schedule_at out keeps the time the post already has. On a queued post this endpoint never publishes anything and never turns a scheduled post into an immediate one, so fixing a typo is only ever a typo fix. A draft is the one thing it can send, and only when the request asks: publish: true sends it now, schedule_at queues it. See Drafts.

Only a post that has not published yet can be edited: a scheduled post or a draft. A post that is publishing, published, failed or deleted is refused with 409 post_not_editable, and so is a draft somebody else has already promoted.

Editing does not count against your quota.

Rescheduling one post of a thread moves the whole thread. Every part of a scheduled thread shares one publish time, so a new time applies to all of them and the response lists them in rescheduled_post_ids. Text and media, by contrast, belong to the one part you named.

Example

curl -X PATCH https://chirpie.ai/api/v1/posts/a1b2c3d4-... \
  -H "Authorization: Bearer chirpie_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "Now with the typo fixed"}'

Move it to a new time instead, changing nothing else:

curl -X PATCH https://chirpie.ai/api/v1/posts/a1b2c3d4-... \
  -H "Authorization: Bearer chirpie_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"schedule_at": "2027-04-02T09:00:00Z"}'

Response 200 OK

The post as it now stands, in the same shape as every other post object. When a schedule_at was given, rescheduled_post_ids names every post that moved.

{
  "data": {
    "id": "a1b2c3d4-...",
    "platform": "x",
    "account_id": "e5f6g7h8-...",
    "text": "Now with the typo fixed",
    "media_urls": null,
    "status": "scheduled",
    "scheduled_at": "2027-04-01T14:00:00.000Z",
    "published_at": null,
    "created_at": "2027-03-30T10:00:00.000Z"
  }
}

Errors

StatusCodeCondition
400bad_requestNothing to change, an unknown field, text or media the platform refuses, a time in the past, or a time within 5 minutes of another post on the same account
400bad_requestpublish together with schedule_at, draft: false, or publish or draft on a post that is not a draft
400unsupported_mediaA media URL the platform cannot accept. Nothing is changed
400first_comment_unsupportedA first_comment on a post whose platform has no first comment. Nothing is changed
400configuration_unsupportedA publishing option the post's platform or the new placement does not carry, or text or a first comment left on a post becoming a story. Nothing is changed
402x_link_posts_require_paid_planNew text adds a link to an X post on the Free plan, or a new first_comment adds one
404not_foundPost doesn't exist, belongs to another user, or its account is no longer connected
409post_not_editableThe post is publishing, published, failed or deleted, so it can no longer be edited, or the draft has already been promoted
429usage_limit_exceededPromoting the draft would cross your monthly post quota or your scheduled-post quota. The draft is left exactly as it was

Retry a First Comment

Post a first comment that failed, again. The request has no body: it re-sends the text the post already carries.

The text cannot be changed once the post is out. PATCH /api/v1/posts/:id refuses a published post with 409 post_not_editable, so write or change a first comment while the post is still a draft or still queued. If the platform keeps refusing the comment, post it yourself.

POST /api/v1/posts/:id/first-comment
Authorization: Bearer chirpie_sk_YOUR_KEY
Idempotency-Key: retry-first-comment-a1b2c3d4

An optional Idempotency-Key makes a retry of this call safe: the same key on the same post replays the original answer for 24 hours rather than sending a second comment. See Idempotency.

curl -X POST "https://chirpie.ai/api/v1/posts/a1b2c3d4-.../first-comment" \
  -H "Authorization: Bearer chirpie_sk_YOUR_KEY"

Response 200 OK

The post, in the same shape as every other post object, with first_comment.status now posted.

{
  "data": {
    "id": "a1b2c3d4-...",
    "platform": "x",
    "status": "published",
    "first_comment": {
      "text": "Full write-up: https://example.com",
      "status": "posted",
      "comment_id": "6f1c2d3e-...",
      "error": null
    }
  }
}

A retry that works costs one post from your monthly quota, exactly as the first attempt would have.

Errors

StatusCodeCondition
404not_foundPost doesn't exist or belongs to another user
404first_comment_not_foundThe post has no first comment to send
409first_comment_not_retryableThe first comment is already posted, the post has not published yet, or another attempt is in flight
502first_comment_failedThe platform refused the retry. The post is untouched, and the message says what happened

A retry runs every check the first attempt ran, so it can also answer with any of those refusals: 429 usage_limit_exceeded when the month's posts are spent, 429 first_comment_rate_limited when the platform is throttling posts from the account, 402 x_link_posts_require_paid_plan for a link on the Free plan, 409 comment_permission_required when the account needs reconnecting, and 501 platform_coming_soon for a platform that is not open yet. The post stays published in every case.


Delete a Post

DELETE /api/v1/posts/:id
Authorization: Bearer chirpie_sk_YOUR_KEY

On a published post, delete means delete on the platform. Chirpie tells the platform first, and reports the post deleted only once the platform confirms it is gone. A post the platform says it has never heard of, or has already removed, counts as gone.

On a post that has not gone out, there is nothing to tell a platform. A queued post is cancelled and its quota returned, and the answer names every row that went with it in cancelled_ids. A draft is simply marked deleted and the answer names its rows in deleted_ids: nothing is returned, because a draft never counted against anything in the first place.

The post is never removed from Chirpie. It keeps its id, its text and its history, with status: "deleted". GET /api/v1/posts/:id still returns it, ?status=deleted lists it, and the dashboard shows it as "Deleted". Nothing a delete does is ever a hole in your record.

Which platforms Chirpie can delete from

PlatformDeleteWhat happens
XYesDELETE /2/tweets/{id}. A post already gone counts as deleted
BlueskyYesdeleteRecord, which is idempotent
LinkedIn (profile and Pages)YesDELETE /rest/posts/{urn}, idempotent
ThreadsYesNeeds the delete permission the connect asks for. An account connected long ago may need reconnecting first, and the error says so
MastodonYesDELETE /api/v1/statuses/{id}
Facebook (feed)YesDeletes the Page post
Facebook (story)NoFacebook publishes no delete for a Page story. 501 delete_unsupported, and the story disappears on its own 24 hours after it was posted
TelegramYes, within 48 hoursTelegram refuses to delete a message more than 48 hours old, and the error says so
PinterestYesDELETE /v5/pins/{id}
YouTubeYesDeletes the video
Google Business ProfileYesDeletes the local post
Instagram (connected via Facebook)YesDeletes the post, whatever the placement. A carousel is deleted whole
Instagram (connected through Instagram)NoChirpie cannot delete a published post from an account connected that way. 501 delete_unsupported. Connect the account via Facebook if you want deletes
TikTokNoTikTok publishes no delete for a post made through its API. 501 delete_unsupported

A failed platform delete is reported, not hidden. If the platform refuses or is unavailable, nothing changes: the post keeps its current status, stays in your listings, and the call returns 502 upstream_error with a message naming the platform. Retry the same request. Chirpie never answers deleted: true for a post that is still public.

Where a delete cannot reach the platform it is refused, not faked. Chirpie cannot delete a published TikTok post, nor a published post from an Instagram account connected through Instagram rather than via Facebook, and Facebook publishes a delete for a Page feed post but not for a Page story, so the call returns 501 delete_unsupported and nothing changes. Delete the post in the platform's own app, or wait for a Page story to expire 24 hours after it was posted. If you only want it out of your Chirpie listings, hide it: that is reversible and reaches no platform.

Cancelling one post in a scheduled thread cancels the thread. A thread is published as a unit, so it is cancelled as a unit too: deleting any post of a scheduled thread cancels every post in it. The response lists the ids in cancelled_ids. Nothing is billed for a thread that never published, and the whole thread's quota is returned.

Response 200 OK

{
  "data": {
    "id": "a1b2c3d4-...",
    "deleted": true
  }
}

Cancelling a post that belongs to a scheduled thread returns every id it cancelled:

{
  "data": {
    "id": "b2c3d4e5-...",
    "deleted": true,
    "cancelled_ids": ["a1b2c3d4-...", "b2c3d4e5-...", "c3d4e5f6-..."]
  }
}

Errors

StatusCodeCondition
404not_foundPost doesn't exist or belongs to another user
501delete_unsupportedChirpie cannot delete a published post on this platform (TikTok), on this account (an Instagram account connected through Instagram rather than via Facebook), or on this placement (a Facebook Page story). Nothing changed. Delete it in the platform's own app, and hide it here if you do not want to see it
502upstream_errorThe platform refused the delete or was unavailable. Nothing changed: the post keeps its status and is still in your listings. Retry the same request

Hide a Post

POST /api/v1/posts/:id/hide
POST /api/v1/posts/:id/unhide
Authorization: Bearer chirpie_sk_YOUR_KEY

Hiding takes a post out of your Chirpie listings. Nothing reaches the platform. The post stays exactly as it is, keeps its analytics and its comments, and unhide puts it back. Nothing is charged and no quota moves.

This is the answer to "I do not want to see this any more". DELETE is the answer to "take it down", and the two are never the same request. Hiding is also the only thing on offer for a post Chirpie cannot delete, such as a published TikTok post.

A hidden post is left out of GET /api/v1/posts unless you pass include_hidden=true, on every filter. It is always readable by id with GET /api/v1/posts/:id, and every post payload carries hidden.

A thread, or a fan-out made in one request, is hidden as the one thing it was made as, so hidden_ids can name more posts than the one you passed. Unhiding a post that was never hidden succeeds and changes nothing.

Hiding a queued post does not stop it publishing. Hide only decides what Chirpie shows you; the scheduler pays no attention to it, so a hidden scheduled post goes out at its time as normal. To stop one going out, delete it: DELETE /api/v1/posts/{id} cancels a post that has not published yet and returns its quota.

curl -X POST "https://chirpie.ai/api/v1/posts/a1b2c3d4-.../hide" \
  -H "Authorization: Bearer chirpie_sk_YOUR_KEY"

Response 200 OK

{
  "data": {
    "id": "a1b2c3d4-...",
    "hidden": true,
    "hidden_ids": ["a1b2c3d4-...", "b2c3d4e5-...", "c3d4e5f6-..."]
  }
}

Errors

StatusCodeCondition
404not_foundPost doesn't exist or belongs to another user

On this page