Skip to content
curl-x
twitterclicurlautomationdeveloper

Batch-Download Tweet Videos With a 5-Line Shell Script

Save a list of public tweet URLs to a text file and pull every video with a short curl loop. Includes a PowerShell version and rate-limit etiquette.

Share:

Want to try it now? Paste a post link from any supported platform to download its media instantly.

Open Downloader

The short version: put your public tweet URLs in a file called urls.txt, one per line, then loop over it with curl -OJ against curl-x — each line downloads that post's video to disk, no browser involved. A basic version is five lines of Bash; add -f and a small sleep and it's solid enough to run unattended.

Key takeaway: curl-x streams a tweet's media directly to a CLI client that requests a post path (curl -OJ https://www.curl-x.com/<user>/status/<id>), so a shell loop over a list of URLs becomes a personal archiving script with no API keys and no extra tooling. This only works for public posts, and it's meant for a personal batch of links you already have — a research folder, a bookmarks export, one thread's worth of clips — not for crawling an account's entire history.

Table of contents

Why this works: the curl-x path trick

Every X/Twitter post on curl-x has a normal HTML page at /<username>/status/<tweetId>, and a sibling route that streams the raw media file. Middleware checks the User-Agent header: if it recognizes a CLI HTTP client (curl, wget, httpie, aria2, axel, lftp), it rewrites the request to the download route and streams the video back with a Content-Disposition header. Browsers and social-preview crawlers still get the normal HTML page — only declared CLI clients get the file.

Practically, the same URL behaves two different ways depending on who's asking:

curl -OJ https://www.curl-x.com/NASA/status/1234567890123456789

-O saves the response to a file instead of printing it; -J uses the filename the server suggests in Content-Disposition rather than guessing from the URL. Once that one command works for a single tweet, batching is just a loop over a list of URLs.

Step 1: build your urls.txt file

Collect the tweet URLs you want, one per line, in a plain text file:

https://x.com/NASA/status/1234567890123456789
https://twitter.com/username/status/2345678901234567890
https://www.curl-x.com/anotheruser/status/3456789012345678901

A few notes on the file:

  1. Both x.com and twitter.com links work — curl-x accepts either domain for the same post path.
  2. You can list curl-x.com URLs directly; the script below works either way since it only cares about the path after the domain.
  3. Skip profile URLs, search links, or anything without /status/<numeric id> — those aren't individual posts.
  4. Only add posts you can already open in a browser. If a post is private, suspended, or deleted, no downloader can pull its media, and that line will just fail.

Step 2: the 5-line Bash loop

Here's the minimal version:

#!/usr/bin/env bash
while read -r url; do
  path="${url#*//*/}"
  curl -OJ "https://www.curl-x.com/${path}"
  sleep 2
done < urls.txt

What each line does:

  1. while read -r url; do … done < urls.txt reads the file one line at a time into $url.
  2. path="${url#*//*/}" strips the scheme and domain, leaving just <username>/status/<tweetId>.
  3. curl -OJ "https://www.curl-x.com/${path}" rebuilds the URL on the curl-x domain and streams the file to disk.
  4. sleep 2 pauses two seconds before the next request (more on why below).

Run it with bash download-tweets.sh. Files land in your current directory, named from whatever Content-Disposition the server sent.

Step 3: make it resilient — retries and -f

The five-line version works, but a dropped connection or a temporarily missing post can leave a stray HTML error page saved as if it were a video. Two curl flags fix most of that:

#!/usr/bin/env bash
while read -r url; do
  [ -z "$url" ] && continue
  path="${url#*//*/}"
  if ! curl -fJO --retry 3 --retry-delay 5 "https://www.curl-x.com/${path}"; then
    echo "failed: ${url}" >> failed-urls.txt
  fi
  sleep 2
done < urls.txt

What's new here:

  • -f (--fail) makes curl exit with an error instead of saving a 404 or 429 error page as if it were the video file.
  • --retry 3 --retry-delay 5 retries transient failures — dropped connections, momentary 5xx responses — three times, five seconds apart.
  • [ -z "$url" ] && continue skips blank lines so an empty line doesn't become a broken request.
  • Failed URLs get logged to failed-urls.txt instead of vanishing, so you can inspect or re-run just the ones that didn't work.

If a URL keeps failing after retries, open it in a browser — that quickly tells you whether the post is gone, private, or image-only.

Step 4: be a good citizen — rate limits and sleep

curl-x runs a per-IP rate limiter at the edge: 60 requests per minute across /api/* and every /download-suffixed route, which is exactly what the CLI rewrite above hits. Go over that and you get an HTTP 429 with a Retry-After: 60 header instead of your file.

For a personal archiving list — a few dozen tweets, a research folder, one thread's worth of clips — a sleep 2 between requests keeps you nowhere near that ceiling and is also just considerate: it spreads requests out instead of hammering the CDN and extractor in a tight burst. On a longer list, sleep 3 or sleep 5 costs almost nothing in wall-clock time and adds a comfortable margin.

This script is for downloading posts you already have links to, not for discovering or enumerating an account's tweets. curl-x has no "list all posts from this user" endpoint, and nothing here should be pointed at scraping a whole timeline — it works one known, public post URL at a time, the same as pasting each link into the site by hand. If you do hit a 429, increase the sleep value and, for a large batch, split it across a few separate runs.

The PowerShell equivalent

If you're on Windows without a Bash shell, the same loop works in PowerShell using Invoke-WebRequest:

Get-Content urls.txt | ForEach-Object {
    if ([string]::IsNullOrWhiteSpace($_)) { return }
    $path = $_ -replace '^https?://[^/]+/', ''
    $target = "https://www.curl-x.com/$path"
    try {
        Invoke-WebRequest -Uri $target -OutFile (Split-Path $target -Leaf) -UserAgent "curl/8.0"
    } catch {
        Add-Content failed-urls.txt $_
    }
    Start-Sleep -Seconds 2
}

A couple of Windows-specific notes:

  • Invoke-WebRequest doesn't read Content-Disposition filenames the way curl's -J does, so this version derives a filename from the URL path instead. If curl.exe is available (it ships with Windows 10 and later), you can skip the cmdlets entirely and call curl.exe -fJO --retry 3 --retry-delay 5 $target inside the same loop.
  • The explicit -UserAgent "curl/8.0" matters: without a recognized CLI user agent, curl-x serves the HTML page instead of the media file.
  • Start-Sleep -Seconds 2 is PowerShell's sleep 2 — keep it for the same rate-limit and etiquette reasons.

What this script will not do

To keep expectations honest:

  • It only handles public posts. curl-x cannot access private or protected accounts' media, with or without a script.
  • It downloads one post per line you provide. There's no crawling, no "get everything from this account," and no bypass for posts that don't exist or were removed.
  • It follows the platform's normal media rules: a post with no native video (text-only, image-only, an external embed) fails the same way it would through curl-x's web form.
  • It's for personal-scale batches — tens of links, not thousands. An unattended script pointed at a large number of accounts' worth of posts is scraping, not archiving.

FAQ

Do I need curl installed, or does this only work on Linux/Mac?

curl ships by default on macOS and modern Linux, and Windows 10/11 include curl.exe too, so the Bash version works as-is on most systems with a shell. The PowerShell version is for anyone who prefers native cmdlets over calling curl.exe directly.

Why did one of my URLs save an HTML file instead of a video?

Either the CLI user agent wasn't sent (check -UserAgent in PowerShell), or the post has no downloadable video — an image-only post, a quote tweet where the video lives in the quoted post, or a link that isn't an exact /status/<id> post URL. Adding -f to curl stops the error page from being saved silently in the first place.

How many tweet URLs can I put in one file?

There's no hard limit in the script, but treat it as a personal list, not a scraping job — a handful to a few dozen URLs is the intended use. For hundreds, split into smaller batches run at different times rather than one long unattended run, and keep the sleep delay in place.

What happens if I go over the rate limit?

You'll get an HTTP 429 with a Retry-After: 60 header instead of the file. With -f set, curl reports this as a failure and logs the URL to failed-urls.txt instead of saving the error page as media. Increase your sleep interval and re-run just the failed URLs.

Can I use this same pattern for other platforms curl-x supports?

Yes — the same rewrite happens for Instagram, TikTok, Facebook, Threads, and Reddit post paths (for example /instagram/<shortcode> or /tiktok/<id>). The loop logic is identical; only the path pattern in urls.txt changes.

Bottom line

A shell script for batch-downloading tweets doesn't need to be complicated: read a list of public post URLs, rewrite each onto curl-x's domain, and let curl -OJ do the work. Adding -f and a couple of retries makes it resilient to occasional failures, and a small sleep between requests keeps you well under the site's 60-requests-per-minute limit while being considerate of the service everyone shares. Keep the list to posts you already have links for and to a personal scale, and this becomes a five-minute setup that saves you from pasting the same URL into a browser tab over and over.

If you're new to the underlying trick, the tweetpath shortcut guide explains the domain-swap idea in a browser context first. And if you're building an archive over time rather than a one-off batch, how to archive public Twitter media before it disappears covers the broader workflow this script fits into.

Ready to download from any platform?

Try curl-x — free, fast, and no login required.

Download Now
Share: