Media
Upload images and videos, attach them to posts, and describe them with alt text.
How do I attach an image or a video?
Two ways, and you can mix them in the same post. Give Chirpie a public URL and it fetches the file for you, or upload the file itself and attach the id it gives back. Either way the same per-platform rules apply, and anything the target platform cannot accept is refused before the post goes out.
POST /api/v1/media
Authorization: Bearer chirpie_sk_YOUR_KEY
Content-Type: multipart/form-dataBoth delivery models end up in the same media field on a post:
{
"account_id": "550e8400-e29b-41d4-a716-446655440000",
"text": "Two pictures, one posted from a file and one from a link.",
"media": [
{ "id": "7c3f1a2b-4d5e-6f70-8192-a3b4c5d6e7f8", "alt": "A bird on a branch" },
{ "url": "https://example.com/second.png", "alt": "The same bird, closer" }
]
}Upload a file
POST /api/v1/media
Authorization: Bearer chirpie_sk_YOUR_KEY
Idempotency-Key: upload-hero-2026-11-01
Content-Type: multipart/form-dataAn optional Idempotency-Key makes a retry of the upload safe: the same key with the same
request replays the original response for 24 hours rather than storing a second copy, and the
replay carries Idempotent-Replay: true. Max 255 characters. See
Idempotency.
Send the file as a part named file. The file type is read from the file's own
first bytes, never from its name or the content type you declare, so a
mislabelled file is refused rather than published.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
file | file part | Yes | The image or video. Up to 4 MB. |
You can also send a JSON body, which is what a client that cannot build a multipart request uses. Name either a public URL or the bytes themselves:
| Field | Type | Required | Description |
|---|---|---|---|
url | string | No | A public image or video URL for Chirpie to fetch and store. |
data | string | No | The file's bytes, base64 encoded. Up to 3 MB of file. Use this or url, not both. |
filename | string | No | A name to show in the dashboard. Never used to decide the file type. |
Every shape is validated identically: the type is read from the file's own bytes, so base64 is not a way past the checks a file part gets.
Base64 costs a third in transit, so that form carries 3 MB of file where a
file part carries 4 MB. A url is not an upload, so neither of those
applies: Chirpie fetches it, up to 20 MB for an image and 512 MB for a video,
and the target platform's own limit applies on top when you attach it to a
post.
Example
curl -X POST https://chirpie.ai/api/v1/media \
-H "Authorization: Bearer chirpie_sk_YOUR_KEY" \
-F "file=@./screenshot.png"Response 201 Created
{
"data": {
"id": "7c3f1a2b-4d5e-6f70-8192-a3b4c5d6e7f8",
"url": "https://blob.chirpie.ai/media/.../7c3f1a2b.png",
"mime_type": "image/png",
"media_type": "image",
"bytes": 184320,
"width": 1200,
"height": 630,
"filename": "screenshot.png",
"created_at": "2026-04-01T13:55:00.000Z",
"expires_at": "2026-04-08T13:55:00.000Z"
}
}An upload is a staging area, not a library. The id is good for 7 days,
which is expires_at, after which it is cleaned up and you upload the file
again to reuse it.
A post that already carries it is unaffected either way. It keeps its own copy of the bytes, so it publishes as written; and while it is still waiting to publish it keeps the id alive too, so a post you scheduled for next month can still be edited in six weeks. The id is released when the post publishes, is deleted, or fails.
Uploads are limited to 4 MB. That is a limit on the upload request, not on
what you can post: a larger image or video goes by URL instead. Put the link in
media_urls, or in a media entry's url, and Chirpie fetches it. There is
no size limit on that path beyond the target platform's own, which is up to
512 MB of video on X.
Error Responses
| Status | Code | Condition |
|---|---|---|
400 | unsupported_media | The file is not an image or video Chirpie can post |
400 | bad_request | No file part, and no url or data in a JSON body |
401 | unauthorized | Missing or invalid API key |
403 | insufficient_scope | The API key does not hold media:write. See Scopes |
409 | idempotency_in_progress | An identical upload with the same Idempotency-Key is still running. Retry for the replay |
413 | unsupported_media | The file is over 4 MB, or over 3 MB as base64 |
422 | idempotency_key_reused | The same Idempotency-Key was used for a different request |
429 | media_limit_reached | Too many uploads are waiting to be posted |
503 | media_storage_failed | The file could not be stored. Try again in a moment |
Attach media to a post
Every endpoint that takes media takes the same three fields, and you use one of them, not several:
| Field | Type | Description |
|---|---|---|
media | object[] | Uploaded files and public links, each with optional alt text. An entry carries either id or url. |
A post reads back as media: [{ url, alt, media_id }]. On an edit, send an uploaded item back as { id: media_id } rather than by its url: the id is the handle that stays valid. | ||
media_ids | string[] | Ids from an upload, when no alt text is needed. |
media_urls | string[] | Public URLs, exactly as before. |
media_urls is unchanged and keeps working. Sending two of the three is
refused with 400 bad_request, because they are three spellings of one field
and there is no single answer to what is attached.
These work identically on POST /api/v1/posts, each part of
POST /api/v1/threads, and
PATCH /api/v1/posts/:id.
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": [
{ "id": "7c3f1a2b-4d5e-6f70-8192-a3b4c5d6e7f8", "alt": "The new dashboard" }
]
}'Error Responses
| Status | Code | Condition |
|---|---|---|
400 | bad_request | Two media fields in one request, or an entry with both id and url |
400 | unsupported_media | The platform cannot accept the file, or the alt text is too long |
404 | media_not_found | The media id is not yours, or has expired |
Alt text
Alt text describes an image or a video for people using a screen reader. Put it on the media item:
{ "media": [{ "url": "https://example.com/chart.png", "alt": "Revenue doubled in Q1" }] }Chirpie sends it to every platform that stores one, and sends nothing when you leave it out. A description is never invented for you.
| Platform | Carries alt text | Longest accepted |
|---|---|---|
| X/Twitter | Yes | 1,000 characters |
| Bluesky | Yes | 2,000 characters |
| Yes | 4,000 characters | |
| Mastodon | Yes | 1,500 characters |
| Yes, on a feed post | 1,000 characters | |
| Yes, on a feed post | 1,000 characters | |
| Threads | No | n/a |
| Telegram | No | n/a |
A platform with no alt text field ignores it. The rest of the post is
unaffected, and nothing else about the request changes. Alt text longer than
the platform stores is a different matter: that is refused with
400 unsupported_media rather than published cut in half. An Instagram story
or reel and a Facebook Page story carry no alt text, so a description sent with
one is kept in Chirpie and not published.
A placement changes the media rules. An Instagram story or reel and a Facebook Page story each take exactly one file, and video where the feed takes none. See Instagram publishing options and Facebook publishing options.
Which delivery model should I use?
| Use | When |
|---|---|
media_urls, or a media entry with url | The file already has a public URL, or is larger than 4 MB |
Upload, then media with id | The file is on your machine, behind auth, or generated on the fly |
Uploading is also what the CLI, the
SDK and the MCP server use when you hand them a local
file, so chirpie post --media ./shot.png needs nothing else from you.
Scheduled posts keep their own copy either way. Media is downloaded, validated and stored when the post is created, not at publish time, so an unusable file is reported when you schedule it and an expired link cannot break a post that is already queued.
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.
Accounts
List and connect the X/Twitter, Bluesky, LinkedIn, Threads, Mastodon, Instagram, Facebook, and Telegram accounts you post through with Chirpie.