Skip to content
curl-x
curlclideveloperapihow-it-workshow-to

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.

Share:

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

Open Downloader

When 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 the User-Agent identify 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

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:

PlatformExample 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:

  • curl
  • wget
  • httpie
  • aria2
  • axel
  • lftp

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:

  1. 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).
  2. Pick the primary media item — first video or first image on multi-item posts.
  3. Select the best downloadable variant (highest-bitrate MP4 for video, largest image rendition for photos).
  4. Stream bytes from the platform CDN through curl-x's shared media-proxy core, with Content-Disposition: attachment and a filename like curl-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:

  1. The URL stays canonical. Scripts can keep using curl-x.com/user/status/id without learning a second path shape. The /download suffix is an implementation detail.
  2. No extra round trip. A rewrite is handled inside the Worker; the client sends one request and receives one response stream.
  3. -L is 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.

CapabilityCLI post URLBrowser UI/api/extract
Single primary media fileYesYesYes
Pick specific photo in carouselNoYes (/photo/2, etc.)Yes (all items in JSON)
Quality picker for videoBest variant auto-selectedYesYes (all variants listed)
Private / deleted posts404404404 with error code
Multi-file zip of all itemsNoPer-item downloadsLoop 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:

LayerEntry pointReturnsBest for
CLI shortcutGET post URL with CLI User-AgentRaw file bytesOne video, one script, one agent tool call
Extract APIPOST /api/extract with JSON {"url": "…"}Structured JSON with all media + variantsCarousels, 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.

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.

Ready to download from any platform?

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

Download Now
Share: