Glossary

The X API INIT APPEND FINALIZE Media Upload Flow, Step by Step

Taras Shynkarenko
Taras Shynkarenko
•Updated: •8 min read
The X API INIT APPEND FINALIZE media upload flow across three endpointsThe X API INIT APPEND FINALIZE media upload flow across three endpoints

TL;DR, Quick Answer

8 min read

Chunked video upload on X v2 is four calls: POST /2/media/upload/initialize returns a media id, POST /2/media/upload/{id}/append takes each chunk with a segment_index, POST /2/media/upload/{id}/finalize closes the session, and GET /2/media/upload?command=STATUS polls processing_info until state is succeeded or failed. The old command=INIT and command=APPEND parameters are gone from everything except that status GET. segment_index caps at 999 and X recommends 5 MB chunks, which does not reach the 16 GB total_bytes the initialize schema accepts.

What is the X API INIT APPEND FINALIZE media upload flow?

Four calls make up the X API INIT APPEND FINALIZE media upload flow: three POSTs that move the file and one GET that reports on what X did with it afterwards. X lists them in that order on the chunked upload guide.

  1. INIT, POST /2/media/upload/initialize, which X describes as "start the session and get a media_id"
  2. APPEND, POST /2/media/upload/{id}/append, "upload each chunk"
  3. FINALIZE, POST /2/media/upload/{id}/finalize, "complete the upload"
  4. STATUS, GET /2/media/upload, "wait for processing when processing_info is returned"

The command names survived the move to v2 but the transport did not. X puts the warning directly under the list: "Do not send command=INIT, command=APPEND, or command=FINALIZE to POST /2/media/upload. Those command-style parameters were the previous upload protocol. The v2 flow uses the dedicated paths above. command=STATUS is still used only on the status GET."

So one of the four commands still exists as a query parameter and three do not. Code written against v1.1 fails on the three POSTs and keeps working on the poll, which is a confusing way to break.

All four calls sit behind the same OAuth 2.0 scope, media.write, including the status GET. Reading the processing state of your own upload needs a write scope, and X publishes no read-only alternative.

What does each step send and return?

Each step carries different parameters, and only two of the four return anything you keep.

StepEndpointYou sendYou get back
INITPOST /2/media/upload/initializemedia_type, total_bytes, media_category, optional additional_owners and sharedid, media_key, expires_after_secs
APPENDPOST /2/media/upload/{id}/appendmedia and segment_indexexpires_at
FINALIZEPOST /2/media/upload/{id}/finalizenothing but the path idid, media_key, size, expires_after_secs, processing_info
STATUSGET /2/media/upload?command=STATUS&media_id=media_id, optional commandid, media_key, size, processing_info, video or image

INIT takes a JSON body, not form fields, and X says so in the guide: "Send a JSON body, not multipart form fields." The media_type enum runs from video/mp4 and video/quicktime through text/srt and text/vtt to two 3D formats most people never notice, model/gltf-binary and model/vnd.usdz+zip. The total_bytes field has a hard schema ceiling of 17179869184, which is 16 GiB.

{
  "data": {
    "id": "1880028106020515840",
    "media_key": "13_1880028106020515840",
    "expires_after_secs": 86400
  }
}

That expires_after_secs of 86400 in X's own example is the session clock: one day from INIT to attach the finished media to something. The id matches the pattern ^[0-9]{1,19}$, and the media_key prefixes it with a type number and an underscore, the same 13_ shape the Ads API documents when it calls the value a video id.

APPEND is the only step whose response tells you nothing useful about the media. Its schema holds a single field, expires_at, described as "Epoch seconds when the upload session expires." Every other step reports the remaining life of the session as expires_after_secs, a relative count. APPEND reports it as an absolute timestamp. Same fact, two units, three field spellings across four endpoints.

FINALIZE is the emptiest request in the flow. It takes the path id and nothing else, no body and no query parameters, which is why the whole curl fits on two lines:

curl -X POST "https://api.x.com/2/media/upload/1880028106020515840/finalize" \
  -H "Authorization: Bearer $USER_ACCESS_TOKEN"

Its response is where the real branch happens. X puts the rule in one sentence: "If processing_info is returned, proceed to Step 4 to wait for processing. If not, the media is ready to use." An image usually skips step four. A video never does.

A person recording video on a phone, the kind of file that gets split into chunks for upload.

How does segment_index work?

segment_index is a plain counter you own, starting at zero, and the schema pins both ends of it: "minimum: 0, maximum: 999."

X's guidance on chunk size sits in the guide instead of the schema: "Keep each segment at or below 5 MB (the server maximum is 8 MB). Segments are indexed from 0." The Python sample in the same guide reads the file in 4 MB blocks and increments the index after each successful call.

curl -X POST "https://api.x.com/2/media/upload/1880028106020515840/append" \
  -H "Authorization: Bearer $USER_ACCESS_TOKEN" \
  -F "segment_index=0" \
  -F "media=@/path/to/chunk0.mp4"

The body can be multipart form data as above or JSON, because the request schema is registered for both content types. In JSON the media field takes base64. In multipart it takes raw bytes.

Now multiply the two published ceilings together. One thousand segments at the 8 MB server maximum is 8 GB. One thousand segments at the recommended 5 MB is 5 GB. Meanwhile POST /2/media/upload/initialize accepts total_bytes up to 16 GiB, and X states plainly that an X Premium or verified account may upload a 16 GB video on a Post. Those two facts cannot both be satisfied through APPEND as documented, and X publishes no larger segment size, no alternative path and no note that the 999 cap moves for large files. Anything above 8 GB has no documented route into the flow.

AdaptlyPost
AdaptlyPost

Start Your 7-Day Free Trial

All-platform analytics

Social Inbox

AI-powered assistant

The upload is also not required to be sequential in any published sense. X lists "Failed chunks can be retried individually" and "Uploads can be paused and resumed" among the advantages of chunking, but never says whether a retried segment overwrites the earlier one, whether indexes may arrive out of order, or what happens if you skip a number. The same question comes up on other networks: Instagram's resumable upload session has an offset header that every published example sets to zero, and LinkedIn answers it by handing you a list of upload URLs, one per part.

A loading screen on a laptop, similar to the progress a server reports while it finishes processing a file.

Segment math against the 16 GB ceiling
1,000 segments at 5 MB (recommended)5 GB
1,000 segments at 8 MB (server maximum)8 GB
total_bytes schema ceiling16 GB
Even at the 8 MB server maximum, 1,000 segments cap out at 8 GB, half of what the initialize schema allows.

What does the STATUS command return?

STATUS returns a processing_info object with three fields, and the one you act on is state.

FieldWhat the schema says
state"Processing state (pending, in_progress, failed, succeeded)"
check_after_secs"Seconds to wait before polling status again"
progress_percent"Processing completion percentage"

The guide prints the happy path as an arrow diagram: "pending to in_progress to succeeded or failed." Two of those four are terminal. X's own polling sample breaks the loop on succeeded and on failed, and sleeps for check_after_secs on anything else, which is the correct shape because the server sets the interval rather than your backoff policy. The FINALIZE example returns "check_after_secs": 1, so the first poll comes fast.

curl "https://api.x.com/2/media/upload?command=STATUS&media_id=1880028106020515840" \
  -H "Authorization: Bearer $USER_ACCESS_TOKEN"

Two gaps are worth knowing before you build the loop.

The four state values live in a description string, not in an enum. The schema types state as a plain string with no allowed-values list, so the four names X prints are documentation rather than a closed set. Treat an unrecognised state as "keep polling" instead of as an error.

The bigger one is that processing_info has nowhere to put a reason for failure. Its schema declares exactly three properties and then additionalProperties: false, so a response that says "state": "failed" carries no code, no message and no subcode alongside it. The v1.1 protocol returned an error object inside processing_info with a name and a message. The v2 schema as published has no field for that, which leaves "failed" as the entire diagnosis.

progress_percent is documented on every processing_info schema in the media reference and appears in none of X's example responses, so plan for it to be absent rather than zero.

Why does a finished upload still fail on POST /2/tweets?

Because X checks the file twice against two different people, and says so twice. The media introduction opens with it: "Upload and posting are separately enforced. A file that uploads successfully can still be rejected when you attach it to a Post."

The first check runs against the uploading user at INIT and FINALIZE. The second runs against the posting user when the media_id reaches POST /2/tweets. X names the failure exactly:

{
  "title": "Forbidden",
  "detail": "This user is not allowed to post a video longer than 20 minutes.",
  "type": "about:blank",
  "status": 403
}

Twenty minutes is the default figure in that string. X notes that the number moves: "N is the posting user's duration cap (20 minutes by default, 125 minutes for Premium / verified)." Which means the same media_id, finalized once and processed to succeeded, attaches for one account and returns 403 for another. Entitlement follows the account, and the tiers that decide which video length an account gets are the same X Premium tiers, not your developer plan.

Post composition limits apply at the same moment. X allows "up to 4 photos, 1 animated GIF, or 1 video" on a Post, and a media_id that violates that mix fails at attach even though the upload itself was clean. The practical sequence for anything long is the full walkthrough for getting high quality video onto X, with the 403 handled as a normal outcome rather than an exception.

Frequently asked questions

Which endpoints replace command=INIT and command=APPEND on the X API v2?

POST /2/media/upload/initialize and POST /2/media/upload/{id}/append. X states that the command-style parameters "were the previous upload protocol" and must not be sent to POST /2/media/upload any more.

Is command=STATUS still valid on the X API v2?

Yes, and it is the only one that is. X writes that "command=STATUS is still used only on the status GET", meaning GET /2/media/upload?command=STATUS&media_id=. The schema marks the command parameter as optional even though every published example passes it.

What is the maximum segment_index on the X API append endpoint?

  1. The append request schema sets segment_index to a minimum of 0 and a maximum of 999, so a file is capped at 1000 chunks.

How large can each APPEND chunk be?

X recommends 5 MB and names 8 MB as the server maximum. Its own Python sample reads the file in 4 MB blocks.

AdaptlyPost
AdaptlyPost

Start Your 7-Day Free Trial

All-platform analytics

Social Inbox

AI-powered assistant

What are the X media upload processing states?

pending, in_progress, succeeded and failed. X prints the progression as pending to in_progress to succeeded or failed, though the schema types state as an open string rather than an enum.

Why does a media_id that finalized successfully fail on POST /2/tweets?

Upload and posting are enforced separately against different entitlements. If the posting user cannot attach a video that long, the Post request returns 403 Forbidden with the detail "This user is not allowed to post a video longer than N minutes."

How long is an X media upload session valid before it expires?

X's own INIT example returns an expires_after_secs of 86400, one day from INIT to attach the finished media to something. FINALIZE reports the same kind of remaining time as expires_after_secs too. APPEND breaks that pattern and returns expires_at, an absolute epoch timestamp instead of a countdown, so the same session clock shows up in two different units.

Does uploading a photo to X require polling the STATUS endpoint?

Usually not. X's rule is that if FINALIZE returns no processing_info block, the media is ready to use, and an image upload usually comes back without one. A video never does, so step four is in practice a video-only wait.

Does the STATUS endpoint use a different OAuth scope than the upload steps?

All four calls, INIT, APPEND, FINALIZE, and STATUS, sit behind the same media.write scope. Reading the processing state of your own upload needs a write scope, and X publishes no read-only alternative.

What media types can you upload through the X API initialize endpoint?

The media_type enum on INIT runs from video/mp4 and video/quicktime through text/srt and text/vtt to two 3D formats, model/gltf-binary and model/vnd.usdz+zip. That range covers video, subtitle files, and 3D assets in a single upload endpoint.

Was This Article Helpful?

Let us know what you think!

See us more often in Google

One click marks AdaptlyPost as a preferred source, so our articles sit higher in your Top Stories, AI Mode, and AI Overviews.

Before you go...

AdaptlyPost

AdaptlyPost

Schedule your content across all platforms

Manage all your social media accounts in one place with AdaptlyPost.

All-platform analytics

Social Inbox

AI-powered assistant

Related Glossary Terms

Related Articles