networking · difficulty ◆◆
curl — transfer data with URLs
The terminal's web browser — and its API client, downloader, and debugger.
Every API you have ever called from a browser was probably tested first with curl — the tool that has shipped with every Unix-like system since the late 1990s and still has no real rival for talking to a server from a terminal.
AI-generated draft · reviewed and editedThis text was generated with AI as a first draft, then reviewed and edited by the author, who holds editorial responsibility for it.$ curlWhat it does
curl transfers data to or from a server using a URL. It supports HTTP, HTTPS, FTP, SFTP, and a dozen other protocols, but in practice you use it for one thing: making HTTP requests from the command line. Give it a URL and it fetches the response and prints it to your terminal. Give it -X POST and a body and it sends data instead. It is the Swiss-army knife of talking to servers.
Why it matters
curl is how you test an API without opening a browser, download a file without a GUI, check whether a server is actually responding, and debug what a web request really sends and receives. When a website 'doesn't work', curl is the tool that tells you whether the problem is the network, the server, or the app. And because it is scriptable and present on every Unix box, it is the backbone of countless shell scripts and CI pipelines.
Exit codes, and the -f flag your scripts want
curl's exit code describes the transfer, not the page: a 404 is a perfectly successful transfer, so `curl -s -o /dev/null -w '%{http_code}'` prints 404 while the shell still sees 0. Add -f (--fail) when a bad HTTP status should be an error, and -S alongside -s so the error text is not suppressed along with the progress bar. That is the pair to put in a cron job or a CI step: a missing page then fails the command instead of silently succeeding with an error page in the output.
Example
$ curl -s -o /dev/null -w "HTTP %{http_code} | %{size_download} bytes | %{time_total}s\n" https://example.comHTTP 200 | 577 bytes | 0.096898s-s silences the progress bar, -o /dev/null discards the body, and -w prints a custom summary. This is the canonical 'is this site up and how fast?' one-liner. (Verified live on this box: 577 bytes in 0.0969 s — the byte count follows example.com's current page and the time changes on every run.)
$ curl -s https://api.github.com/repos/curl/curl | head -n 7{
"id": 569041,
"node_id": "MDEwOlJlcG9zaXRvcnk1NjkwNDE=",
"name": "curl",
"full_name": "curl/curl",
"private": false,
"owner": {A GET request to a JSON API. curl prints the raw response body — here the first seven lines of GitHub's metadata for the curl/curl repository (id 569041). Pipe the whole response into jq to pretty-print it and pull out fields you actually want.
$ curl -s -o /dev/null -w "%{http_code}\n" -X POST -H "Content-Type: application/json" -d '{"name":"test"}' https://httpbin.org/post200-X POST sends a POST request, -H adds a header, -d sends a JSON body. The -w '%{http_code}' prints just the status code, which is how scripts check whether an API call succeeded.
$ curl -s -I https://example.comHTTP/2 200
date: Thu, 15 Oct 2026 10:30:09 GMT
content-type: text/html; charset=utf-8
server: cloudflare
last-modified: Tue, 13 Oct 2026 22:05:48 GMT
allow: GET, HEAD
accept-ranges: bytes
age: 6
cf-cache-status: HIT
cf-ray: a4ae259848d528d0-VIE
alt-svc: h3=":443"; ma=86400-I sends a HEAD request, which fetches only the response headers, not the body. This is the fastest way to inspect a server's headers, caching, and content type. (Verified live: date, age, cf-cache-status and cf-ray are generated per request, so yours will differ.)
$ curl -sS -f https://example.com/404test; echo exit=$?curl: (22) The requested URL returned error: 404
exit=22Without -f the same URL exits 0 — curl reports that the transfer worked, not that the page exists. With -f, an HTTP error becomes exit code 22. (Verified live on this box; the bare `-s -o /dev/null -w '%{http_code}'` version prints 404 and still exits 0, which is exactly the trap -f closes.)
Common flags
- -s
- silent — suppress the progress bar and error messages (essential for scripts)
- -o FILE
- write the response body to a file instead of stdout (use -o /dev/null to discard it)
- -O
- save the body to a file named after the URL's last path segment (like wget)
- -X METHOD
- set the HTTP method explicitly, e.g. -X POST, -X PUT, -X DELETE
- -H HEADER
- add a request header, e.g. -H "Content-Type: application/json"
- -d DATA
- send data in the request body (implies POST); use -d @file.json to read from a file
- -I
- send a HEAD request — fetch only the response headers
- -w FORMAT
- print a custom summary after the transfer, e.g. -w "%{http_code} %{time_total}s"
History
Origin
curl's line starts in November 1996, when the Brazilian developer Rafael Sagula released HttpGet 0.1. Daniel Stenberg was writing an IRC bot and wanted currency rates fetched from a web page automatically; he fixed a couple of bugs in HttpGet, sent the fixes back, and within a release or two had taken over maintenance. When FTP support arrived the name was wrong, so it became urlget 2.0 in August 1997; when uploads arrived that name was wrong too, and on 20 March 1998 Stenberg shipped curl 4.0, carrying the old version numbers over. He still leads the project today.
The name and the reach
The name was Stenberg's choice: the word contains 'URL', it can be read as 'see URL' — which is what the tool does, show you what is at a URL — and it is a real English word, so it is pronounced 'curl'. The reach came later and steadily: curl was in Linux distributions and the BSD ports collections by the early 2000s, was bundled with Mac OS X 10.1 in 2001 and with Windows 10 in 2018, and the libcurl library underneath is embedded in countless applications, package managers, and devices.
Fun facts
Pros & cons
pros
- + Universal — ships with every Unix-like system, macOS, and modern Windows, no install needed
- + Handles HTTP, HTTPS, FTP, and a dozen more protocols with one consistent interface
- + Scriptable and pipe-friendly: -s, -o, and -w make it perfect for CI and shell automation
- + Full control over headers, methods, bodies, and redirects — nothing hidden from you
cons
- − Output is raw by default — JSON and HTML come out unformatted until you pipe through jq or a pager
- − Does not follow redirects unless you remember -L, which trips up beginners
- − The flag set is huge and the syntax can feel cryptic compared to a GUI browser
Takeaways
- 1Check a site is up and fast: `curl -s -o /dev/null -w "%{http_code} %{time_total}s\n" URL`
- 2Download a file: `curl -O URL` saves it with the server's filename
- 3Follow redirects with -L or many links will silently fail
- 4Test an API: `curl -X POST -H "Content-Type: application/json" -d @payload.json URL`
- 5Inspect headers only with -I — the fastest way to see a server's response metadata
- 6Pipe JSON into jq for readable output: `curl -s URL | jq .`
