Networking and remote
curl
Transfer data to or from a URL; the standard tool for HTTP requests, API calls and downloads from the shell.
curl [OPTION...] URL...
curl URL sends a request and writes the response body to standard output. Everything else is
options: -o to save to a file, -L to follow redirects, -i to see the response headers,
-d or --json to send a body, -H for headers, -u for credentials. It speaks HTTP(S),
FTP, SFTP and many other protocols, but in practice it is the command line's HTTP client.
Two defaults surprise people. curl does not follow redirects unless you pass -L, and it
treats an HTTP error such as 404 or 500 as a successful transfer: it prints the error page and
exits 0. In scripts, add -f (or --fail-with-body) so failures produce a non-zero exit
status, and -sS so the progress meter disappears but real errors are still printed.
The examples talk to a small test server on localhost:8080 inside the sandbox; the same
commands work against any host. For mirroring sites or resuming big downloads with minimal
typing, wget is the alternative.
Examples
Fetch a URL and print the body
curl -s http://localhost:8080/api/status.json
{"status": "ok", "version": "2.4.1", "uptime_s": 86400} The response body goes to standard output. -s (silent) hides the progress meter, which
curl draws on standard error whenever its output goes to a pipe or a file, as it does
here; at an interactive terminal plain curl URL prints just the body.
Save to a file with -o or -O
curl -s -o servers.json http://localhost:8080/api/servers.json
curl -O http://localhost:8080/files/report.csv
ls -l servers.json report.csv
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
100 405 100 405 0 0 102k 0 --:--:-- --:--:-- --:--:-- 131k
-rw-r--r-- 1 dev dev 405 Oct 31 20:19 report.csv
-rw-r--r-- 1 dev dev 503 Oct 31 20:19 servers.json -o NAME chooses the file name; -O reuses the last part of the URL (report.csv). When
saving to a file curl shows its progress meter, as the second command does: total size,
percent received and average speed.
Follow redirects with -L
curl -s -o /dev/null -w '%{http_code} -> %{redirect_url}\n' http://localhost:8080/old-page
curl -sL http://localhost:8080/old-page
301 -> http://localhost:8080/index.html <!doctype html> <html><head><title>Demo server</title></head> <body><h1>It works</h1><p>Local test server for curl and wget examples.</p></body></html>
/old-page answers 301 with a Location header and an empty body, and plain curl stops
there. With -L it requests the new location and prints the real page.
Show only the response headers
curl -sI http://localhost:8080/index.html
HTTP/1.1 200 OK Server: demo-httpd/1.0 Date: Sat, 31 Oct 2026 20:19:32 GMT Content-type: text/html Content-Length: 152 Last-Modified: Mon, 26 Oct 2026 13:48:10 GMT
-I sends a HEAD request, so the server returns the status line and headers but no
body: a quick way to check a status, content type, size or caching headers.
Show headers and body together
curl -si http://localhost:8080/api/status.json
HTTP/1.1 200 OK
Server: demo-httpd/1.0
Date: Sat, 31 Oct 2026 20:19:32 GMT
Content-type: application/json
Content-Length: 56
Last-Modified: Mon, 26 Oct 2026 13:48:10 GMT
{"status": "ok", "version": "2.4.1", "uptime_s": 86400} -i prints the response headers, a blank line, then the body. Unlike -I, this is a normal
GET.
Make HTTP errors fail with -f
curl -s http://localhost:8080/status/404; echo "exit: $?"
curl -sSf http://localhost:8080/status/404; echo "exit: $?"
{
"status": 404
}
exit: 0
curl: (22) The requested URL returned error: 404
exit: 22 Without -f, a 404 is still a successful transfer: curl prints the error body and exits 0.
With -f it prints nothing from the body, reports the status (shown because of -S) and
exits 22, which set -e or && can act on.
Fail but keep the error body
curl -sS --fail-with-body http://localhost:8080/status/503
curl: (22) The requested URL returned error: 503
{
"status": 503
} --fail-with-body (curl 7.76 and later) exits 22 like -f but still prints the response,
which often explains what went wrong.
Print the status code and timing
curl -s -o /dev/null -w '%{http_code} %{size_download} bytes %{time_total}s\n' http://localhost:8080/index.html
200 152 bytes 0.001301s
-o /dev/null discards the body and -w prints a template after the transfer. Variables
such as %{http_code}, %{size_download} and %{time_total} turn curl into a health check
or a timing probe.
Send form data with -d
curl -s -d name=web03 -d role=web http://localhost:8080/api/servers
{
"method": "POST",
"path": "/api/servers",
"headers": {
"User-Agent": "curl/8.14.1",
"Accept": "*/*",
"Content-Type": "application/x-www-form-urlencoded"
},
"body": "name=web03&role=web"
} -d makes the request a POST with Content-Type: application/x-www-form-urlencoded,
joining several -d values with &. The test server echoes back what it received.
Send JSON with --json
curl -s --json '{"name": "web03", "cpus": 2}' http://localhost:8080/api/servers
{
"method": "POST",
"path": "/api/servers",
"headers": {
"User-Agent": "curl/8.14.1",
"Content-Type": "application/json",
"Accept": "application/json"
},
"body": "{\"name\": \"web03\", \"cpus\": 2}"
} --json (curl 7.82 and later) posts the data and sets both Content-Type and Accept to
application/json. On older curl, write
-H 'Content-Type: application/json' -d '...'. --json @file.json reads the body from a file.
Choose the method and add headers
curl -s -X DELETE -H 'X-Request-Id: abc123' -H 'Accept: application/json' http://localhost:8080/api/servers/web03
{
"method": "DELETE",
"path": "/api/servers/web03",
"headers": {
"User-Agent": "curl/8.14.1",
"X-Request-Id": "abc123",
"Accept": "application/json"
},
"body": ""
} -X sets the method name and each -H adds a header (or replaces one curl would send,
like Accept). The echo shows both arrived.
Build a query string safely
curl -s -G --data-urlencode 'q=disk usage > 90%' -d limit=5 http://localhost:8080/api/echo
{
"method": "GET",
"path": "/api/echo?q=disk+usage+%3e+90%25&limit=5",
"headers": {
"User-Agent": "curl/8.14.1",
"Accept": "*/*"
},
"body": ""
} -G turns the -d data into a query string on a GET instead of a POST body, and
--data-urlencode encodes the value for you: in the echoed path, spaces became +, >
became %3e and % became %25.
HTTP Basic authentication with -u
curl -s http://localhost:8080/private/report.txt
curl -s -u alice:s3cret http://localhost:8080/private/report.txt
{
"error": "authentication required"
}
Quarterly report: confidential Without credentials the server answers 401 with a JSON error. -u user:password sends a
Basic Authorization header. Leave out :password and curl prompts for it, which keeps it
out of your shell history.
See the request and response headers with -v
curl -sv -o /dev/null http://localhost:8080/api/status.json 2>&1 | grep '^[<>]'
> GET /api/status.json HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/8.14.1 > Accept: */* > < HTTP/1.1 200 OK < Server: demo-httpd/1.0 < Date: Sat, 31 Oct 2026 20:19:32 GMT < Content-type: application/json < Content-Length: 56 < Last-Modified: Mon, 26 Oct 2026 13:48:10 GMT <
-v traces the exchange on standard error: > lines are what curl sent, < lines what
came back. The grep keeps just those, dropping the connection details; a full -v also
shows DNS results, connection attempts and TLS details.
Retry a failing connection
curl -sS --retry 2 --retry-delay 1 --retry-connrefused http://localhost:9999/
curl: (7) Failed to connect to localhost port 9999 after 0 ms: Could not connect to server curl: (7) Failed to connect to localhost port 9999 after 0 ms: Could not connect to server curl: (7) Failed to connect to localhost port 9999 after 0 ms: Could not connect to server
Nothing listens on port 9999. curl makes three attempts in all, one second apart, printing
an error for each, then gives up with exit status 7 ("failed to connect"). Plain --retry only retries transient errors
such as timeouts and 5xx answers; --retry-connrefused adds refused connections.
Common pitfalls
curl exits 0 on HTTP 404 or 500. Scripts should use
-for--fail-with-body, and check$?.-salone also hides error messages, so a failing download looks like an empty result. Use-sStogether.-X POSTdoes not add a body and-X GET -d ...still sends a body. Let-d,--json,-Gand-Ichoose the method; use-Xonly for methods likePUT,PATCHorDELETE.Without
-L, a URL that redirects (for example http to https) yields an empty body and status 301 or 302.Quote URLs that contain
?or&: unquoted, the shell treats&as "run in background" and cuts the URL short.
GNU vs BSD and macOS
The examples above ran the GNU/Linux version. These are the differences that matter on macOS and the BSDs (written from their documentation, not run here).
curl is the same program on Linux, macOS and Windows 10+, but versions differ:
--jsonneeds 7.82 and--fail-with-bodyneeds 7.76. Checkcurl --version.
Options worth knowing
A selection, described in our own words. For the full list, see curl(1) or curl --help.
| Option | What it does |
|---|---|
-o, --output FILE | Write the body to FILE instead of standard output. |
-O, --remote-name | Save to a file named like the last part of the URL. |
-L, --location | Follow redirects. |
-I, --head | Send a HEAD request and show only the headers. |
-i, --include | Print the response headers before the body. |
-s, --silent | Hide the progress meter and error messages. |
-S, --show-error | With -s, still print error messages. |
-f, --fail | Exit with status 22 and no body on HTTP errors (400 and above). |
--fail-with-body | Exit with status 22 on HTTP errors but still print the body. |
-v, --verbose | Trace the connection, request and response headers on standard error. |
-w, --write-out FORMAT | Print FORMAT after the transfer, with variables like %{http_code}. |
-d, --data DATA | Send DATA as a POST body; @file reads it from a file. |
--data-urlencode DATA | Like -d, but percent-encode the value. |
--data-binary DATA | Send DATA exactly as given, without stripping newlines. |
--json DATA | POST DATA as JSON and set the JSON Content-Type and Accept headers. |
-F, --form NAME=VALUE | Send a multipart form field; NAME=@file uploads a file. |
-G, --get | Put -d data into the URL query string and use GET. |
-H, --header HEADER | Add or replace a request header. |
-X, --request METHOD | Use METHOD instead of the default. |
-u, --user USER[:PASSWORD] | Send credentials; prompts for the password if omitted. |
-A, --user-agent STRING | Set the User-Agent header. |
-e, --referer URL | Set the Referer header. |
-b, --cookie DATA|FILE | Send cookies from a string or a cookie file. |
-c, --cookie-jar FILE | Save received cookies to FILE. |
-k, --insecure | Skip TLS certificate verification (testing only). |
-C, --continue-at OFFSET | Resume a transfer; use - to work out the offset from the existing file. |
-T, --upload-file FILE | Upload FILE (PUT for HTTP). |
--retry NUM | Retry transient failures up to NUM times. |
--retry-delay SECONDS | Wait this long between retries. |
--retry-connrefused | Also retry when the connection is refused. |
-m, --max-time SECONDS | Give up if the whole operation takes longer. |
--connect-timeout SECONDS | Give up if connecting takes longer. |
-x, --proxy URL | Use the given proxy. |
-4, --ipv4 | Resolve names to IPv4 addresses only. |
--compressed | Ask for a compressed response and decompress it. |
Related
wgetDownload files over HTTP, HTTPS and FTP, non-interactively, with retries, resuming and recursive mirroring.ncOpen raw TCP or UDP connections, or listen for them; for testing ports, talking to services by hand and piping data between hosts.jqQuery, filter and reshape JSON from the command line with a small expression language.sshLog in to a remote machine or run a command on it over an encrypted connection; also forwards ports and jumps through hosts.