Skip to content

Twitter video API: get the MP4 URL of any X video

A Twitter video's MP4 URL sits in the post's media data: X encodes each upload into up to five MP4 renditions, from 270p to 1080p or more, and lists every file's URL with its bitrate.

Short answer: Take the MP4 with the highest bitrate; a GIF is one silent MP4. The official X API lists renditions only with media.fields=variants, at $10.00 per 1,000 video posts from November 22, 2026. Sorsa API returns each post's video link through POST /tweet-info-bulk, 100 posts per request from $0.014 per 1,000, and 100 free requests cover the first 10K.

Endpoint
POST /tweet-info-bulk
Page size
100 posts per request
Price
from $0.014 per 1,000

#How X stores a video: renditions and bitrates

X never serves the file the author uploaded. It re-encodes every video into a ladder of MP4 files, one per size, and an HLS playlist (an .m3u8 index that video players use to switch sizes during playback). Each MP4 is a complete file with sound, so any single one downloads and plays on its own. For one post, the free Twitter Video Downloader saves the file from a pasted link; this guide is for getting the files in code.

On November 2, 2026 we read the rendition lists of 23 public posts from 2017 to 2026 and measured the 72 MP4 files X served for them. The bitrate X lists for each file is a ceiling, and the real files run three to five times leaner:

RenditionFrame size, pixelsBitrate X listsReal average bitrate
270p480 × 270256 kbpsabout 91 kbps
360p640 × 360832 kbpsabout 233 kbps
720p1280 × 7202,176 kbpsabout 626 kbps
1080p1920 × 108010.4 Mbpsabout 2,172 kbps
  • 270p

    Frame size, pixels
    480 × 270
    Bitrate X lists
    256 kbps
    Real average bitrate
    about 91 kbps
  • 360p

    Frame size, pixels
    640 × 360
    Bitrate X lists
    832 kbps
    Real average bitrate
    about 233 kbps
  • 720p

    Frame size, pixels
    1280 × 720
    Bitrate X lists
    2,176 kbps
    Real average bitrate
    about 626 kbps
  • 1080p

    Frame size, pixels
    1920 × 1080
    Bitrate X lists
    10.4 Mbps
    Real average bitrate
    about 2,172 kbps
The four MP4 files X served for one 82-second 1080p video, real size against listed bitrate One 82-second video: the four MP4 files X served for it Real file Predicted 270p 256 kbps listed 1.1 MB 2.6 MB 360p 832 kbps listed 2.9 MB 8.5 MB 720p 2,176 kbps listed 7.3 MB 22.2 MB 1080p 10.4 Mbps listed 22.2 MB 105.7 MB Solid bar: the file X served. Dashed outline, predicted: listed bitrate × 82 seconds. Measured on video.twimg.com, November 2, 2026.
The 1080p file of an 82-second video weighed 22.2 MB, about a fifth of what its listed bitrate predicts.

What the measurements show:

  • The highest bitrate is the best file. Sort the MP4 entries by bitrate and take the top one. The playlist entry, typed application/x-mpegURL, carries no bitrate and is an index, not a file.
  • Size depends on the frame shape. Vertical videos get 320 × 568 up to 1080 × 1920, square ones 320 × 320 up to 1080 × 1080, and one 1936 × 1548 upload got a fifth file at 25.1 Mbps.
  • Not every 1080p upload gets a 1080p file. Of seven 1920 × 1080 uploads from 2025 and 2026, two stopped at 720p, and most posts from before 2022 top out at 720p.
  • The playlist splits sound from picture. Its video streams are silent; audio comes as separate tracks at 32, 64 and 128 kbps, with subtitles on some videos.

A GIF is simpler: X keeps one silent, looping MP4 at the GIF's own size, with no other renditions and no playlist.

#Get a video's MP4 URL with Sorsa API

Sorsa API, a Twitter (X) API and scraper, returns every post with its media attached: each video, GIF and photo is an entry in the post's entities list with a type, a direct link and a preview image. One API key in a header replaces the developer account and OAuth that the official X API asks for.

  1. Get a key. Sign up and copy the API key from your dashboard. The first 100 requests are free, with no card.

  2. Send the post's link. POST /tweet-info (opens in a new tab) takes a post URL or a bare ID in tweet_link.

    Terminal
    curl -X POST "https://api.sorsa.io/v3/tweet-info" \
      -H "ApiKey: YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"tweet_link":
           "https://x.com/northfield_video_lab/status/1984519203377641472"}'
    
    Response200 OK
    {
      "id": "1984519203377641472",
      "created_at": "2026-10-31T16:04:11Z",
      "full_text": "First ride on the new gravel loop.",
      "entities": [
        {
          "type": "video",
          "link": "https://video.twimg.com/amplify_video/1984519110025023488/vid/avc1/480x270/Qm3vTb8xLwK2pRfA.mp4?tag=21",
          "preview": "https://pbs.twimg.com/amplify_video_thumb/1984519110025023488/img/Hx2LkP9aQwErT0bn.jpg"
        }
      ],
      "user": {
        "username": "northfield_video_lab",
        "display_name": "Northfield Video Lab",
        "media_count": 214
      }
    }
    
  3. Read the video entries. Keep entities whose type is video and whose link is on video.twimg.com. A /tweet_video/ path in the link marks a GIF.

  4. Download the file. The MP4 is a plain HTTPS file on X's servers: no key, no cookie, and no request counted.

    import requests
    
    API = "https://api.sorsa.io/v3/tweet-info"
    HEADERS = {"ApiKey": "YOUR_API_KEY"}
    
    
    def video_links(post):
        r = requests.post(API, headers=HEADERS,
                          json={"tweet_link": post}, timeout=60)
        r.raise_for_status()  # 403: no key, or no requests left
        tweet = r.json()
        for e in tweet.get("entities") or []:
            if e.get("type") == "video" and "video.twimg.com" in e["link"]:
                yield tweet["id"], e["link"]
    
    
    links = video_links("1984519203377641472")
    for n, (post_id, link) in enumerate(links, 1):
        with open(f"{post_id}_{n}.mp4", "wb") as f:
            f.write(requests.get(link, timeout=120).content)
    

The link is an MP4 with sound that plays anywhere. We ran two videos through our free downloader, which runs on Sorsa API, on November 2, 2026, and both links were X's 270p rendition. For the 82-second one that meant 1.1 MB: light enough for previews, transcripts, frame sampling and large archives. A GIF's link is its only file, so it arrives whole.

The preview doubles as a full-size frame: add ?format=jpg&name=orig to it (drop the .jpg first) and X returns the frame at the upload's own size, 1936 × 1548 pixels for one of our test posts. Photos come in the same list with type photo; getting each one at its original size, for one post or a whole account, is covered in the Twitter image scraper guide.

For a list of posts, send up to 100 links or IDs in tweet_links to POST /tweet-info-bulk (opens in a new tab). The response is {"tweets": [...]} with the same entities in every post, and the whole batch is one request: from $0.014 per 1,000 posts.

#Download every video from an account or a search

Search finds the video posts for you. X's search operator filter:native_video keeps posts with a video uploaded to X, and card_name:animated_gif keeps GIFs. POST /search-tweets (opens in a new tab) returns about 20 matching posts per request, with search reaching back to 2006.

JobQuery
Every video of one accountfrom:northfield_video_lab filter:native_video
Every GIF of one accountfrom:northfield_video_lab card_name:animated_gif
Videos on a topic in one month"gravel racing" filter:native_video since:2026-10-01 until:2026-11-01
Popular videos only#bikepacking filter:native_video min_faves:1000
Videos and YouTube linkstrail build filter:videos
  • Every video of one account

    Query
    from:northfield_video_lab filter:native_video
  • Every GIF of one account

    Query
    from:northfield_video_lab card_name:animated_gif
  • Videos on a topic in one month

    Query
    "gravel racing" filter:native_video since:2026-10-01 until:2026-11-01
  • Popular videos only

    Query
    #bikepacking filter:native_video min_faves:1000
  • Videos and YouTube links

    Query
    trail build filter:videos

Every operator, with examples, is in the guide to Twitter search operators. The script below runs one query from the first page to the last.

Downloading every video of an X account with Sorsa API Search the posts /search-tweets filter:native_video about 20 posts a page Read the entities type: "video" link: MP4, sound /tweet_video/ in the link = a GIF Save each file <post id>_1.mp4 straight from video.twimg.com Log it videos.csv id, date, kind, bytes, file next_cursor, until it comes back empty
One search page is one request; the video files come straight from X's servers and cost no requests.
  1. Save the script. It pages the search with next_cursor, a token that marks where the next page starts, until the cursor comes back empty. It writes every post's videos and GIFs to <post id>_<n>.mp4 and logs each file in videos.csv.

    x_videos.py
    import csv
    import os
    import subprocess
    import sys
    import time
    
    import requests
    
    API = "https://api.sorsa.io/v3/search-tweets"
    HEADERS = {"ApiKey": "YOUR_API_KEY"}
    
    
    def search(body):
        while True:
            r = requests.post(API, headers=HEADERS, json=body, timeout=60)
            if r.status_code == 429:  # over 20 requests a second: wait
                time.sleep(1)
                continue
            r.raise_for_status()  # 403: no key, or no requests left
            return r.json()
    
    
    def videos(tweet):
        """(kind, link) for every video and GIF in a post."""
        for e in tweet.get("entities") or []:
            link = e.get("link") or ""
            if e.get("type") == "video" and "video.twimg.com" in link:
                yield ("gif" if "/tweet_video/" in link else "video"), link
    
    
    def save(link, path):
        with requests.get(link, stream=True, timeout=120) as r:
            r.raise_for_status()
            with open(path, "wb") as f:
                for chunk in r.iter_content(1 << 16):
                    f.write(chunk)
    
    
    def save_hd(post_url, n, path):
        """The post's highest-bitrate MP4 through yt-dlp; False if yt-dlp fails."""
        cmd = ["yt-dlp", "-f", "best[protocol=https]", "--playlist-items", str(n),
               "-o", path, post_url]
        done = subprocess.run(cmd, capture_output=True).returncode == 0
        return done and os.path.exists(path)
    
    
    def run(query, folder, hd=False):
        os.makedirs(folder, exist_ok=True)
        state = os.path.join(folder, "cursor.txt")
        body = {"query": query, "order": "latest"}
        if os.path.exists(state):  # resume a stopped run
            body["next_cursor"] = open(state).read().strip()
        log_path = os.path.join(folder, "videos.csv")
        with open(log_path, "a", newline="", encoding="utf-8") as f:
            log = csv.writer(f)
            while True:
                page = search(body)
                for t in page.get("tweets", []):
                    for n, (kind, link) in enumerate(videos(t), 1):
                        path = os.path.join(folder, f"{t['id']}_{n}.mp4")
                        if os.path.exists(path):
                            continue
                        url = f"https://x.com/{t['user']['username']}/status/{t['id']}"
                        if not (hd and kind == "video" and save_hd(url, n, path)):
                            save(link, path)
                        log.writerow([t["id"], t["created_at"], kind,
                                      os.path.getsize(path), path])
                cursor = page.get("next_cursor")
                if not cursor:
                    break
                body["next_cursor"] = cursor
                with open(state, "w") as s:
                    s.write(cursor)
        if os.path.exists(state):
            os.remove(state)
    
    
    if __name__ == "__main__":
        run(sys.argv[1], sys.argv[2], hd="--hd" in sys.argv[3:])
    
  2. Run it with a query and a folder. python x_videos.py "from:northfield_video_lab filter:native_video" northfield saves the account's videos into the folder northfield. A 403 means the key is missing or out of requests; a 429 means more than 20 requests in one second, and the script waits a second and retries.

  3. Run it again any time. Files already on disk are skipped and the cursor is saved after each page. A stopped run picks up where it left off, and a rerun adds only new videos.

A whole account's videos take few requests: at about 20 posts per page, an account with 300 videos is about 15 requests, because the search returns only the posts that carry one. For text and every other post field in a spreadsheet, see how to download all tweets from a user.

#Get the 720p or 1080p file of a post

For a post's 720p or 1080p file, hand its link to yt-dlp, the open-source command-line downloader, which reads public posts without an X login. The format filter below picks the MP4 with the highest bitrate:

Terminal
yt-dlp -f "best[protocol=https]" -o "%(id)s.%(ext)s"   "https://x.com/northfield_video_lab/status/1984519203377641472"

Add --hd to the script above and it does this for every video, falling back to the API's link when yt-dlp fails. On our 82-second test video, yt-dlp 2026.10.26 picked the 1920 × 1080 file. Its format table, run with -F, listed that file at about 100 MiB, because the table multiplies the listed bitrate by the length; the download weighed 22.2 MB. yt-dlp fetches one post at a time from your own address, so Sorsa API does the finding in bulk and yt-dlp only the HD files you need.

Good to know

The official X API returns the whole rendition list in one response, but only if you ask. Add expansions=attachments.media_keys and media.fields=variants to a post lookup, and each video comes back in includes.media with a variants array of bit_rate, content_type and url. It needs a developer account and OAuth, and from November 22, 2026 each media object in that answer is billed at $0.005 on top of the $0.005 post read.

#Twitter GIFs in the API

A GIF posted on X comes back as an MP4, because X converts every GIF into a short, silent, looping video when it is posted. Its link sits on video.twimg.com/tweet_video/, X keeps no other size, and that one file is the best copy there is. In X's own data it is typed animated_gif with a bitrate of 0; in Sorsa API's entities it comes as a video whose link has that /tweet_video/ path.

To turn it back into a .gif file, run ffmpeg with a palette pass, which keeps the colors clean:

Terminal
ffmpeg -i 1211797914437259264_1.mp4 \
  -vf "fps=15,split[a][b];[a]palettegen[p];[b][p]paletteuse" \
  -loop 0 1211797914437259264.gif

Expect a much bigger file: in our test an 83 KB MP4 became a 1.76 MB GIF at the same 498 × 272 pixels, 21 times the size. Keep the MP4 for storage and players, and make the GIF only where a page or a chat needs one. For a single GIF without code, the free Twitter GIF Downloader saves it from a post link.

#What it costs, in requests and in storage

On Sorsa API a post costs the same whatever media it carries. Posts looked up by ID cost from $0.014 per 1,000, 100 per request, and posts found through search from $0.07 per 1,000, about 20 per request. The video files download from X's servers and cost no requests. On the official X API every video arrives through the media expansion, and from November 22, 2026 X bills expansion objects (opens in a new tab) at the standard rate. A video post then costs $0.005 for the post plus $0.005 for its media: $10.00 per 1,000.

Way to get the video linksWhat it needsCost per 1,000 video posts
Sorsa API, posts by IDOne API keyfrom $0.014
Sorsa API, searchOne API keyfrom $0.07
Official X APIDeveloper account and OAuth$10.00 from Nov 22, 2026
  • Sorsa API, posts by ID

    What it needs
    One API key
    Cost per 1,000 video posts
    from $0.014
  • Sorsa API, search

    What it needs
    One API key
    Cost per 1,000 video posts
    from $0.07
  • Official X API

    What it needs
    Developer account and OAuth
    Cost per 1,000 video posts
    $10.00 from Nov 22, 2026
Video posts

Official X API

$10,000

for 1M video posts

$0.010 per video post, from Nov 22

Sorsa API

from$14

for 1M video posts

from $0.014 per 1,000

714x cheaper

The official X API's pay-per-use pricing (opens in a new tab) also stops at 3M post reads per billing cycle, and above that X sells only Enterprise contracts. Sorsa API runs 20 requests per second on every plan, so batches of 100 posts look up 2,000 video posts a second, and failed requests are never charged.

Storage is the bigger bill for a video archive, and it depends on the rendition far more than on the count. At the real average bitrates we measured, 1,000 thirty-second videos take about 0.3 GB at 270p, 2.3 GB at 720p and 8.1 GB at 1080p. Set your own length and count:

seconds
videos
RenditionPer videoAll videosCeiling
270p0.34 MB341 MB0.96 MB
360p0.87 MB874 MB3.1 MB
720p2.3 MB2.3 GB8.2 MB
1080p8.1 MB8.1 GB38.9 MB

Per video: the median real bitrate per rendition (91, 233, 626 and 2,172 kbps). Ceiling: the bitrate X lists (256, 832 and 2,176 kbps, and 10.4 Mbps) times the length.

Sizes per video come from the real average bitrates of 63 MP4 files measured on X's servers on November 2, 2026; the ceiling uses the bitrate X lists.

#Start pulling X videos in code

To get the MP4 URLs of X videos and GIFs in code, use Sorsa API: one request returns up to 100 posts with each video's link, from $0.014 per 1,000. Search finds every video of an account or a topic back to 2006, and your X account is never involved. Copy the script, add your key, and your 100 free requests return the links of your first 10K video posts.

#FAQ

Can the official X API return Twitter video URLs?

Yes. Add expansions=attachments.media_keys and media.fields=variants to a post lookup or search, and each video in includes.media lists its MP4 renditions with bit_rate and url; take the highest bitrate. Without those two parameters you get only a preview image. It needs a developer account and OAuth, and from November 22, 2026 each media object adds $0.005 to the $0.005 post read.

Which API should I use to get Twitter video URLs?

Use Sorsa API. POST /tweet-info-bulk returns up to 100 posts per request with each video's MP4 link and preview, from $0.014 per 1,000 posts. The official X API charges $10.00 per 1,000 video posts from November 22, 2026. Sorsa API needs one API key instead of a developer account and OAuth, finds videos through search back to 2006 and runs at 20 requests per second on every plan.

Why does my downloaded Twitter video look blurry?

You most likely saved one of the smaller renditions. X keeps every video as up to five MP4 files, from 270p to 1080p or more, and some tools and APIs hand over the first or lightest one. Pick the MP4 with the highest bitrate in the list. If the best file is still 720p, the upload got no 1080p rendition: two of seven 1080p uploads we checked from 2025 and 2026 stopped at 720p.

Can I download videos from private accounts or deleted posts?

No. A protected account's posts are visible only to the followers it approves, so no API returns its videos, Sorsa API included, and a request for them is never charged. Deleted posts drop out of X's search and lookups, so no API can list them either; only a copy saved before the deletion keeps the video.

Yes. Saving public videos for research, moderation or an archive is legal and common. The copyright stays with whoever made each video, so get the owner's permission before you publish or resell the files. Sorsa API reads public posts through its own API, so your X account takes no part in it.

#How we checked

On November 2, 2026 we read the rendition lists X publishes for 23 public video and GIF posts from 2017 to 2026 and measured the 72 of their 75 MP4 files that video.twimg.com served. For one video we also read its HLS playlist and listed its formats with yt-dlp 2026.10.26, logged out. We ran three of the posts through our free downloader, which runs on Sorsa API, to see the links it returns. The script ran against a stand-in for the API that served those real links, and the ffmpeg line ran on a real GIF file. Endpoints and fields come from the live Sorsa API reference; X's media fields and prices come from docs.x.com and X's notice of October 23, 2026, read the same day.

By the Sorsa API team. We build and run Sorsa API, the read-only Twitter/X API and scraper, in production since 2022 (formerly TweetScout API) with 6B+ requests served. About Sorsa API

What changed
  • Rewritten from scratch: measured renditions and real bitrates of 72 MP4 files, the Sorsa API route for one post, a list and a whole account with a tested script, GIFs as MP4, a storage calculator and costs that include X's November 22 media billing.
  • First published.