X API Media Upload v2: The Chunked Endpoints Moved, and Every Old Tutorial Is Now a Dead Link
- The command-style v1.1 upload - one endpoint with INIT, APPEND and FINALIZE as parameters - has been replaced by real REST paths: POST /2/media/upload/initialize, POST /2/media/upload/{id}/append, POS
- Checked 2026-09-20. The documentation URL itself has moved more than once - two plausible doc paths returned 404 on the day we checked, and the legacy developer.x.com v1.1 page returned HTTP 402 Payme
- Initialize takes a JSON body with media_type, total_bytes and media_category. The docs say to keep each segment at or below 5 MB, noting the server maximum is 8 MB.
- Finalize is not the end. If processing_info comes back you must poll status until it reports succeeded, because attaching a media_id that is still in progress is the most common cause of a post that f
Your posting script worked for two years. Today the first call returns a 404, the error body tells you nothing useful, and every tutorial you open describes an endpoint that no longer answers. Then you try to open X's own legacy documentation to check what changed, and that page returns HTTP 402 Payment Required.
That is not a metaphor - it is literally what we got on 2026-09-20 when we tried to load the old v1.1 chunked upload reference on developer.x.com. The historical record of how this used to work is now behind a paywall, while the copies of it scattered across tutorials and Stack Overflow answers are free and wrong. That combination is why this particular migration has produced so much wasted debugging.
The autocomplete tells the same story from the other side. People searching this are typing the URL itself - the suggestion list for "x api media upload" contains the v2 spelling and a query that is just the API path with the slashes flattened. Nobody searches for a URL unless the one they have stopped working.
The Live Paths, Checked 2026-09-20
The old protocol put everything on one endpoint and switched behaviour with a "command" parameter. The current flow uses dedicated REST paths. X's documentation states it plainly: "Those command-style parameters were the previous upload protocol. The v2 flow uses the dedicated paths above."
| Step | Current v2 path | What the old tutorial told you |
|---|---|---|
| Initialize | POST /2/media/upload/initialize | POST media/upload with command=INIT |
| Append | POST /2/media/upload/{id}/append | POST media/upload with command=APPEND |
| Finalize | POST /2/media/upload/{id}/finalize | POST media/upload with command=FINALIZE |
| Status | GET /2/media/upload | GET media/upload with command=STATUS |
A warning about the date stamp, and please take it seriously. On the day we checked, two entirely plausible documentation URLs for this exact topic returned 404 before we found the one that resolved. The endpoints have moved, and so has the page describing them. Any article about X's media upload that does not tell you when it was verified - including, in six months, this one - should be treated as a hypothesis rather than a reference. Go and confirm against the live docs before you write code against it.
The Four Steps, and the Three Fields That Bite
1. Initialize. A POST to the initialize path with a JSON body carrying media_type, total_bytes and media_category. The first two are mechanical. The third is where integrations silently misbehave, because the category determines what the upload is allowed to be attached to. The documented set covers tweet_image, tweet_gif, tweet_video, amplify_video for ads and promoted video, the three dm_ equivalents, and subtitles. Uploading a video with an image category will not necessarily fail at upload time - it will fail later, somewhere less obvious.
2. Append. One call per chunk, to the append path for that media id. On chunk size the documentation is specific: "Keep each segment at or below 5 MB (the server maximum is 8 MB)." Two numbers in one sentence is a recommendation and a hard ceiling, and the sane reading is the conservative one - size your chunks against the 5 MB guidance, and leave headroom under it for multipart encoding overhead rather than aiming at the maximum and discovering the difference in production.
3. Finalize. The call that closes the upload. It is not the call that makes the media usable.
4. Status - the step that is skipped, and the one that breaks everything. If finalize returns processing_info, the media is still being transcoded. The documented states run pending, then in_progress, then either succeeded or failed. You poll the status endpoint until it lands.
Almost every "my video upload works but the post fails" report is the same bug: the code treated finalize as completion and attached a media id that was still in progress.
This is structurally the same trap as YouTube's upload behaviour, where a successful API call and a visible video are two separate events with a gap in between - we wrote that one up in YouTube API Upload Private: Your Script Worked. Different platform, identical shape: the API answering yes is not the platform saying done.
The Limits We Are Not Going to Guess At
There is a real gap here and we would rather name it than paper over it. X's changelog carries an entry dated 2026-09-01 reading "Media upload and post-creation size/duration limits documented based on user Premium status" - meaning the file-size and duration ceilings now vary by the posting account's Premium tier.
We could not retrieve the numbers behind that entry. The limits pages we tried on docs.x.com returned 404 on 2026-09-20. So this article publishes no per-tier size or duration figures, because we do not have them from a first-party source, and a wrong ceiling in your upload validation is worse than no ceiling at all. If you need them, get them from the live docs and stamp your own date on them. The broader problem of X publishing the same limit three different ways for three different product surfaces is one we have already run into in X API Video Upload Limit: Three Official Numbers, All Correct.
Before You Rewrite the Uploader, Price the Thing
Worth a pause here, because a media-upload migration is usually the moment someone discovers the rest of the bill. Getting the video into X is the technical half. Posting it is the metered half, and the two are charged in different units.
If you are rebuilding this integration to publish at any volume, do the arithmetic on the posting side before you finish the uploader. We have the per-request numbers in What Does It Cost to Post to X Through the API?, what remains of the free tier in X API Free Tier Limits in 2026, and who ends up absorbing that cost when a scheduling vendor sits in the middle in Who Is Paying for the X API Now?.
There is also a branch worth naming, because a surprising number of people rebuild an uploader they did not need. If your goal is publishing your own content to your own accounts rather than building a product for other people, the API is one route and a browser session logged into your own account is another. The API buys you scale, determinism and a support surface; it charges you in tiers, scopes and migrations like this one. Neither is free. The right question is which currency you would rather pay in.
Frequently Asked Questions
Why does my old media/upload code return 404 now?
Because chunked upload moved from a single endpoint driven by a command parameter to four dedicated v2 paths: initialize, append and finalize under /2/media/upload, plus a GET for status. The documentation describes the command-style parameters as the previous upload protocol. Rewriting against the new paths is a structural change, not a parameter rename.
What chunk size should I use for X media upload?
The documentation says to keep each segment at or below 5 MB and notes that the server maximum is 8 MB. Size against the 5 MB figure rather than the ceiling, and leave room under it for multipart encoding overhead so that a chunk that measures under the limit locally does not arrive over it.
My upload finalizes but the post fails. What is wrong?
Most likely you attached the media id before processing finished. If finalize returns processing_info, the media is still transcoding through pending and in_progress before reaching succeeded or failed. Poll the status endpoint until it reports success, then attach the id. Treating finalize as completion is the single most common bug in this flow.
