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.
Want to try it now? Paste a post link from any supported platform to download its media instantly.
Open DownloaderThe 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
- Step 1: build your urls.txt file
- Step 2: the 5-line Bash loop
- Step 3: make it resilient — retries and -f
- Step 4: be a good citizen — rate limits and sleep
- The PowerShell equivalent
- What this script will not do
- FAQ
- Bottom line
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:
- Both
x.comandtwitter.comlinks work — curl-x accepts either domain for the same post path. - You can list
curl-x.comURLs directly; the script below works either way since it only cares about the path after the domain. - Skip profile URLs, search links, or anything without
/status/<numeric id>— those aren't individual posts. - 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:
while read -r url; do … done < urls.txtreads the file one line at a time into$url.path="${url#*//*/}"strips the scheme and domain, leaving just<username>/status/<tweetId>.curl -OJ "https://www.curl-x.com/${path}"rebuilds the URL on the curl-x domain and streams the file to disk.sleep 2pauses 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 5retries transient failures — dropped connections, momentary 5xx responses — three times, five seconds apart.[ -z "$url" ] && continueskips blank lines so an empty line doesn't become a broken request.- Failed URLs get logged to
failed-urls.txtinstead 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-WebRequestdoesn't readContent-Dispositionfilenames the way curl's-Jdoes, so this version derives a filename from the URL path instead. Ifcurl.exeis available (it ships with Windows 10 and later), you can skip the cmdlets entirely and callcurl.exe -fJO --retry 3 --retry-delay 5 $targetinside 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 2is PowerShell'ssleep 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.
Related Guides
How to Download a Twitter Video With curl (One Command)
Fetch a Twitter/X post URL with curl and get the actual MP4 back — no browser, no API key. One command, real filename, works with wget too.
Can You Download a Twitter Profile Picture in Full Size?
Yes — curl-x now downloads X profile pictures natively. Paste the profile link, or curl curl-x.com/pfp/username, and get the original full-size image.
How to Extract Audio From a Twitter Video (MP3 or M4A)
Download a public X video as MP4 with curl-x, then strip the audio track to MP3 or M4A with FFmpeg, VLC, or a browser converter. Step-by-step for Mac, PC, and phone.