X (Twitter)

X API Chunked Media Upload

How chunked media upload works on the X API, and when you need it.

Quick answer

X requires video, animated GIFs, and images over roughly 5 MB to go through media/upload in four steps: INIT declares the byte size and media_category and returns a media_id, APPEND sends chunks up to 5 MB each, FINALIZE starts processing, and STATUS is polled until succeeded. PostGate’s media endpoint wraps the whole state machine.

X still requires video, animated GIFs, and any image over roughly 5 MB to go through media/upload in chunks rather than as one request body. It's a four-step protocol — INIT, APPEND, FINALIZE, STATUS — and PostGate's media endpoint wraps it so you don't implement the state machine yourself. This page covers both: the underlying X protocol and PostGate's equivalent.

01

Why does chunked upload exist at all?

A single request body has practical size limits, and X needs to run asynchronous processing on video and GIFs — transcoding, thumbnail extraction — before the file is usable in a post. Chunking spreads a large upload across many smaller requests, and the finalize/status split gives that processing step somewhere to happen without holding a connection open.
02

What are the four steps on X's side?

INIT declares the total byte size and media_category (e.g. tweet_video) and returns a media_id. APPEND sends each chunk — up to 5 MB per segment — tagged with a segment_index, repeated until every byte is uploaded. FINALIZE tells X the upload is complete and processing can start. STATUS is then polled (for video and GIF; images finalize synchronously) until processing_info.state reaches succeeded, at which point the media_id can be attached to a post.
The shape of the exchange, X media/upload v1.1PostGate API
# INIT
POST /1.1/media/upload.json
  command=INIT&total_bytes=52428800&media_type=video/mp4&media_category=tweet_video
  -> { "media_id_string": "10000000000000001" }

# APPEND (repeat per chunk)
POST /1.1/media/upload.json
  command=APPEND&media_id=10000000000000001&segment_index=0
  media=<chunk-bytes>

# FINALIZE
POST /1.1/media/upload.json
  command=FINALIZE&media_id=10000000000000001
  -> { "processing_info": { "state": "pending", "check_after_secs": 5 } }

# STATUS (poll)
GET /1.1/media/upload.json?command=STATUS&media_id=10000000000000001
  -> { "processing_info": { "state": "succeeded" } }
03

PostGate's equivalent: one media resource, not a state machine

Create a media reservation with filename, MIME type, and byte count, upload each part to the signed URL you get back, then complete it with the returned ETags. PostGate reruns X's own processing and reports ready only once the file is actually attachable — the polling loop happens on PostGate's side, not yours. See the Media resource reference for the full multipart flow shared across every platform.
Start a multipart uploadPostGate API
curl -X POST https://postgate.studio/api/media \
  -H "Authorization: Bearer $POSTGATE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "source": "upload",
    "filename": "launch-clip.mp4",
    "mime": "video/mp4",
    "bytes": 52428800
  }'
04

What X actually accepts, enforced up front

For an X target, PostGate checks images up to 5 MB, MP4 video up to 512 MB and 140 seconds, and a maximum of 4 media items per post before it spends a single write call. A file that fails validation never reaches X, so it never gets billed as a post attempt.
05

Attach it to a post once ready

Reference the media by ID in the same POST /api/posts call used for any platform — see X API: Post a Tweet for the full request shape.
Publish with the uploaded assetPostGate API
curl -X POST "https://postgate.studio/api/posts?profile_group_id=grp_acme123" \
  -H "Authorization: Bearer $POSTGATE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "post": { "body": "New clip is live." },
    "profiles": ["pro_x123"],
    "media": ["med_01JXYZ"]
  }'

Frequently asked questions

When is chunked upload required on X?

For video, animated GIFs, and any image over roughly 5 MB — X requires those to go through media/upload in chunks rather than as one request body.

Do I need to poll STATUS for image uploads?

No — images finalize synchronously. STATUS polling applies to video and GIF, which go through asynchronous processing (transcoding, thumbnail extraction) after FINALIZE; poll until processing_info.state reaches succeeded, at which point the media_id can be attached to a post.

What media limits does X enforce for a post?

For an X target, PostGate checks images up to 5 MB, MP4 video up to 512 MB and 140 seconds, and a maximum of 4 media items per post before it spends a single write call. A file that fails validation never reaches X, so it never gets billed as a post attempt.

How does PostGate replace X’s chunked upload state machine?

Create a media reservation with filename, MIME type, and byte count, upload each part to the signed URL you get back, then complete it with the returned ETags. PostGate reruns X’s own processing and reports ready only once the file is actually attachable — the polling loop happens on PostGate’s side, not yours.

Build it with PostGate

One API for every social platform your customers care about — sandboxed, documented, and ready today.

Start building Read the docs