TL;DR, Quick Answer
8 min readChunked 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.
- INIT,
POST /2/media/upload/initialize, which X describes as "start the session and get amedia_id" - APPEND,
POST /2/media/upload/{id}/append, "upload each chunk" - FINALIZE,
POST /2/media/upload/{id}/finalize, "complete the upload" - STATUS,
GET /2/media/upload, "wait for processing whenprocessing_infois 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.
| Step | Endpoint | You send | You get back |
|---|---|---|---|
| INIT | POST /2/media/upload/initialize | media_type, total_bytes, media_category, optional additional_owners and shared | id, media_key, expires_after_secs |
| APPEND | POST /2/media/upload/{id}/append | media and segment_index | expires_at |
| FINALIZE | POST /2/media/upload/{id}/finalize | nothing but the path id | id, media_key, size, expires_after_secs, processing_info |
| STATUS | GET /2/media/upload?command=STATUS&media_id= | media_id, optional command | id, 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.

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

What does the STATUS command return?
STATUS returns a processing_info object with three fields, and the one you act on is state.
| Field | What 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?
- The append request schema sets
segment_indexto 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
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
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


Every Twitter API media_category Value and What It Permits
The twitter api media_category parameter has eight values, decides the size and duration caps X enforces, and fails at Post time when you pick the wrong one.


The Instagram Resumable Upload Session and the rupload Host
An Instagram resumable upload starts with upload_type=resumable on /media, then a POST to rupload.facebook.com carrying offset and file_size headers.


Meta Documents Four Different Limits for Facebook scheduled_publish_time
The Facebook scheduled_publish_time field needs a time at least 10 minutes out, and Meta gives four maximums for it: 29 days, 30 days, 75 days and 6 months.
Related Articles


How the Meta business use case rate limit Meters Facebook Pages
The Meta business use case rate limit for Pages allows 4800 calls per engaged user every 24 hours on Page and system user tokens, and fails with code 80001.


The Instagram API Image Requirements Meta Enforces at Upload
Meta's Instagram API image requirements are JPEG only, 8 MB maximum and a 4:5 to 1.91:1 aspect ratio, each with its own error code.


The Instagram API Rate Limit Is 4800 Times Your Impressions
Meta sets the Instagram API rate limit at 4800 calls per impression in a rolling 24 hours and reports usage in X-Business-Use-Case-Usage.

