Why curl-x Post URLs Return the File in Terminal
curl a curl-x post URL and get the MP4, not HTML. A design deep-dive on the middleware rewrite, User-Agent gate, and /download routes.
Want to try it now? Paste a post link from any supported platform to download its media instantly.
Open DownloaderWhen you fetch a curl-x post URL from a terminal client, you get the media file itself — an MP4, JPEG, or other attachment — instead of the HTML downloader page a browser would see. That behavior is deliberate: curl-x is named after the trick, and the site rewrites CLI requests internally to a sibling /download route that streams the post's primary media with a proper Content-Disposition filename. Browsers, social-preview crawlers, and anything without a recognized CLI User-Agent still receive the normal page.
This article is for developers, DevOps engineers, and automation authors who want to understand why the same URL behaves differently in curl versus Chrome — not just how to run the one-liner (covered in how to download a Twitter video with curl).
TL;DR: curl-x middleware checks two things before serving a post: (1) is the path a bare media-detail URL like
/NASA/status/123…or/instagram/ABC123, and (2) does theUser-Agentidentify a CLI client (curl,wget,httpie,aria2,axel,lftp)? If both are true, the request is rewritten (not redirected) to${pathname}/download, which resolves public media and streams bytes back. Everyone else gets HTML with Open Graph tags intact.
Table of contents
- The observation: one URL, two responses
- Why curl-x was built this way
- The rewrite gate: path + User-Agent
- What happens on the /download route
- Why a rewrite instead of a redirect
- Who is excluded — and why that matters
- Supported paths across every platform
- What the CLI shortcut cannot do
- How this relates to /api/extract
- Security boundaries the design respects
- FAQ
- Bottom line
The observation: one URL, two responses
Run these two commands against the same public tweet path on curl-x:
# Terminal client → binary media file
curl -I https://www.curl-x.com/NASA/status/1234567890123456789
# Browser-like client → HTML page
curl -I -A "Mozilla/5.0" https://www.curl-x.com/NASA/status/1234567890123456789
The first response typically shows:
HTTP/2 200
content-type: video/mp4
content-disposition: attachment; filename="curl-x.com_1234567890123456789.mp4"
The second shows content-type: text/html — the downloader UI with preview, quality picker, and download buttons.
Same hostname, same path, different User-Agent, different payload. That split is the core design decision behind curl-x's CLI story.
Why curl-x was built this way
Most "Twitter video downloader" sites are browser-first: you paste a URL, click a button, then save a file. That workflow is fine for one-off saves, but it breaks down for:
- Shell scripts archiving a list of public posts
- Cron jobs or CI pipelines pulling reference clips
- AI agents that need a tool returning a file path, not a DOM tree
- Headless servers with no graphical browser
The conventional workaround is a JSON API: sign up, get a key, POST a URL, parse the response, fetch the CDN link yourself. curl-x skips that ceremony for the common case — one public post, one primary media file — by making the post URL itself the download endpoint when a CLI client asks.
The product name is not marketing fluff. The intended usage is literally:
curl -OJ https://www.curl-x.com/<username>/status/<tweetId>
For structured metadata (every variant, every item in a carousel), there is still POST /api/extract. The terminal shortcut and the JSON API are complementary, not competing.
The rewrite gate: path + User-Agent
Two independent checks must pass before curl-x streams a file. Both live in proxy.ts middleware and lib/curl-download.ts.
Check 1: is this a media-detail path?
isMediaDetailPath() matches bare post URLs — one pattern per supported platform — that have a sibling /download route handler:
| Platform | Example path |
|---|---|
| Twitter / X | /NASA/status/1234567890123456789 |
| Instagram post | /instagram/CxYzAbC123 |
| Instagram reel | /instagram/reel/CxYzAbC123 |
| Threads | /threads/DPTlPfaDQ4B |
| Facebook video | /video/1234567890123456 |
| Facebook photo | /photo/1234567890123456 |
| Facebook reel | /reel/1234567890123456 |
| Reddit post | /r/pics/comments/abc123 |
| Reddit gallery | /gallery/abc123 |
| TikTok | /tiktok/7123456789012345678 |
| TikTok (fully qualified) | /@user/video/7123456789012345678 |
| Bluesky | /bluesky/handle.bsky.social/3jx… |
Paths that do not match include the homepage (/), indexed media pages like /photo/1 or /video/1, profile URLs, and anything already ending in /download. The extra /download segment means a rewritten request cannot match again — the rewrite cannot loop.
Check 2: is this a CLI download client?
isCliDownloadAgent() returns true only when the User-Agent header contains one of six allowlisted substrings:
curlwgethttpiearia2axellftp
Matching is a substring check on a lowercase copy of the header. Real curl sends something like curl/8.5.0; wget sends Wget/1.21.4. Both match.
A missing or empty User-Agent returns false. curl-x does not guess. If your HTTP library sends a generic string, set the header explicitly: curl -A "curl/8.0" ….
When both checks pass, middleware clones the request URL, appends /download to the pathname, and calls NextResponse.rewrite(). The browser's address bar would still show the original path if this were a browser request — but CLI clients do not have an address bar. They only see the response body and headers.
What happens on the /download route
Each platform's /download/route.ts follows the same contract:
- Resolve the post's public media via that platform's extractor (syndication JSON, HTML scrape, AppView API — whatever the platform module already uses for the web UI).
- Pick the primary media item — first video or first image on multi-item posts.
- Select the best downloadable variant (highest-bitrate MP4 for video, largest image rendition for photos).
- Stream bytes from the platform CDN through curl-x's shared
media-proxycore, withContent-Disposition: attachmentand a filename likecurl-x.com_<postId>.mp4.
The download route also answers HEAD requests with the same headers minus the body — useful for curl -I preflight checks before pulling a large file.
Upstream fetches use a browser-like User-Agent (Mozilla/5.0 … Chrome/…) because CDNs such as video.twimg.com and cdninstagram.com often reject bare curl/8.x strings. The incoming CLI identity triggers the rewrite; the outgoing fetch pretends to be a normal browser. That two-hop pattern is standard for download proxies and is the same machinery described in how browser-based downloaders work — only the entry point differs.
Why a rewrite instead of a redirect
curl-x could have returned 302 Location: …/download and let the client follow. It does not, for three practical reasons:
- The URL stays canonical. Scripts can keep using
curl-x.com/user/status/idwithout learning a second path shape. The/downloadsuffix is an implementation detail. - No extra round trip. A rewrite is handled inside the Worker; the client sends one request and receives one response stream.
-Lis optional. Following redirects is harmless but unnecessary. Documentation can show a single URL that always works.
This is an internal route rewrite in the Next.js/vinext middleware sense — same as Vercel's rewrites documentation describes for mapping one path to another handler without changing what the client requested.
Who is excluded — and why that matters
The User-Agent allowlist is intentionally tight, not permissive.
Browsers are excluded because Chrome, Safari, and Firefox send Mozilla/… strings. Users clicking a curl-x link in a tab should see the downloader UI — preview, quality options, related posts — not an immediate file dump.
Social crawlers are excluded because link unfurling depends on HTML and Open Graph meta tags. If Twitterbot, facebookexternalhit, Slackbot, or Discordbot received a raw MP4, shared links in Slack, Discord, and iMessage would lose their rich previews. The middleware comment in the codebase calls this out explicitly: crawlers must keep getting the page.
Arbitrary HTTP libraries are excluded unless they identify as one of the six CLI tools. That prevents accidental "download mode" for SDKs that happen to hit a post URL. If you are wrapping curl-x in Python requests or Node fetch, set a CLI User-Agent or use POST /api/extract instead.
Supported paths across every platform
The CLI rewrite is wired on one bare detail path per platform, each mirroring how curl-x names posts internally (not necessarily the original platform's URL shape):
curl https://www.curl-x.com/NASA/status/1234567890123456789 # Twitter / X
curl https://www.curl-x.com/instagram/ABC123xyz # Instagram post
curl https://www.curl-x.com/instagram/reel/ABC123xyz # Instagram reel
curl https://www.curl-x.com/threads/DPTlPfaDQ4B # Threads
curl https://www.curl-x.com/video/1234567890123456 # Facebook video
curl https://www.curl-x.com/reel/1234567890123456 # Facebook reel
curl https://www.curl-x.com/r/pics/comments/abc123 # Reddit
curl https://www.curl-x.com/tiktok/7123456789012345678 # TikTok
curl https://www.curl-x.com/bluesky/user.bsky.social/3jxabc… # Bluesky
You do not paste twitter.com or instagram.com URLs directly into this trick — swap the domain to curl-x.com and keep the curl-x path (which for Twitter means /username/status/id, not the raw x.com string if it differs). The tweetpath shortcut guide covers domain swapping in more detail.
TikTok is a special case at download time: CDN URLs expire and are session-signed, so the /download route re-resolves the post when you fetch rather than proxying a stale stored URL. That is why re-pasting a TikTok link fixes expired downloads, as explained in why TikTok download links expire.
What the CLI shortcut cannot do
Honesty about limits prevents broken scripts.
| Capability | CLI post URL | Browser UI | /api/extract |
|---|---|---|---|
| Single primary media file | Yes | Yes | Yes |
| Pick specific photo in carousel | No | Yes (/photo/2, etc.) | Yes (all items in JSON) |
| Quality picker for video | Best variant auto-selected | Yes | Yes (all variants listed) |
| Private / deleted posts | 404 | 404 | 404 with error code |
| Multi-file zip of all items | No | Per-item downloads | Loop variants yourself |
Indexed paths like /username/status/id/photo/1 are browser-only preview pages. They are not on the isMediaDetailPath allowlist and have no CLI rewrite. For batch work, see batch-downloading tweet videos with a shell script or call /api/extract and fetch each variant URL.
How this relates to /api/extract
Think of two layers:
| Layer | Entry point | Returns | Best for |
|---|---|---|---|
| CLI shortcut | GET post URL with CLI User-Agent | Raw file bytes | One video, one script, one agent tool call |
| Extract API | POST /api/extract with JSON {"url": "…"} | Structured JSON with all media + variants | Carousels, quality choice, multi-platform detection |
Both are unauthenticated. Both respect the same extraction limits — public posts only, same upstream rate limits. The CLI path is fewer moving parts; the API path is more information.
If your agent needs JSON, use the API. If your agent needs a file on disk, use the CLI path. curl-x supports both without an API key because neither endpoint is doing anything a logged-out visitor could not already preview on the open web.
Security boundaries the design respects
The terminal trick does not bypass curl-x's safety model.
SSRF protection: even on /download, proxied CDN URLs must pass ALLOWED_HOSTS in lib/download-proxy.ts — an allowlist of platform CDN hostnames. Arbitrary URLs cannot be fetched through the post URL mechanism.
Rate limiting: the Cloudflare Worker applies per-IP limits — 60 requests/minute across /api/* and /download* routes, with a stricter 20/minute bucket on /api/extract. Scripted loops should include sleep between calls, as documented in the batch-download guide.
No auth escalation: the rewrite only changes which handler serves a URL the client already knew. It does not grant access to private posts, DMs, or subscriber-only content. A 404 in the browser is a 404 in curl.
Size cap: the media proxy refuses upstream Content-Length values above 500 MB before streaming begins, limiting abuse via mis-sized responses.
FAQ
Why do I get HTML when I run curl?
Your client is not sending a recognized CLI User-Agent, or the path is not a bare media-detail URL. Add -A "curl/8.0" or confirm the path matches the table above — no trailing /photo/1, no query-string-only differences that change the path shape.
Is this an HTTP redirect?
No. It is an internal rewrite to ${pathname}/download. The client requests one URL; the server routes it to the download handler without a 302 round trip.
Does curl need -L?
No. -L follows redirects; this design uses rewrites. -OJ is the important flag pair — -O saves to disk, -J uses the filename from Content-Disposition.
Will link previews break if curl-x served files to everyone?
Yes — that is why browsers and social crawlers are excluded. Open Graph tags live on the HTML page; crawlers must receive HTML, not MP4.
Can I use this in production automation?
Yes, for modest batches of public URLs you already have permission to archive. Space requests a few seconds apart, handle 404/502 with retries, and use /api/extract when you need every item in a multi-photo post. See use curl-x as an API for error codes and rate-limit details.
Why six CLI tools and not every command-line HTTP client?
A small allowlist reduces false positives. Tools like httpie, aria2, axel, and lftp are common in download workflows. Expanding the list requires a deliberate code change — not automatic detection of "anything that is not a browser."
Bottom line
Fetching a curl-x post URL from the terminal returns the file itself because middleware recognizes CLI User-Agents on bare media-detail paths and rewrites those requests to a /download handler that streams the post's primary public media. Browsers and link-preview crawlers keep the HTML page. No API key, no redirect, no second URL to memorize — just curl -OJ against the same path you would open in a tab.
For the practical one-liner, start with how to download a Twitter video with curl. For JSON and agents, read use curl-x as an API. For scripting lists of posts, see batch-downloading tweet videos with a shell script.
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.
Use curl-x as an API: No Key, No Login, No Watermark
curl-x has no signup and no API key. Fetch a post URL with curl for a direct file, or POST to /api/extract for structured JSON — here's the real request/response shape, error codes, and rate limits.
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.