ChirpieDocs
API Reference

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-data

Both 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-data

An 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

FieldTypeRequiredDescription
filefile partYesThe 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:

FieldTypeRequiredDescription
urlstringNoA public image or video URL for Chirpie to fetch and store.
datastringNoThe file's bytes, base64 encoded. Up to 3 MB of file. Use this or url, not both.
filenamestringNoA 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

StatusCodeCondition
400unsupported_mediaThe file is not an image or video Chirpie can post
400bad_requestNo file part, and no url or data in a JSON body
401unauthorizedMissing or invalid API key
403insufficient_scopeThe API key does not hold media:write. See Scopes
409idempotency_in_progressAn identical upload with the same Idempotency-Key is still running. Retry for the replay
413unsupported_mediaThe file is over 4 MB, or over 3 MB as base64
422idempotency_key_reusedThe same Idempotency-Key was used for a different request
429media_limit_reachedToo many uploads are waiting to be posted
503media_storage_failedThe 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:

FieldTypeDescription
mediaobject[]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_idsstring[]Ids from an upload, when no alt text is needed.
media_urlsstring[]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

StatusCodeCondition
400bad_requestTwo media fields in one request, or an entry with both id and url
400unsupported_mediaThe platform cannot accept the file, or the alt text is too long
404media_not_foundThe 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.

PlatformCarries alt textLongest accepted
X/TwitterYes1,000 characters
BlueskyYes2,000 characters
LinkedInYes4,000 characters
MastodonYes1,500 characters
InstagramYes, on a feed post1,000 characters
FacebookYes, on a feed post1,000 characters
ThreadsNon/a
TelegramNon/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?

UseWhen
media_urls, or a media entry with urlThe file already has a public URL, or is larger than 4 MB
Upload, then media with idThe 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.

On this page