opscape

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...

15 of 15 examples run in Debian 13.7 on 2026-10-31 · curl 8.14.1-2+deb13u7 · manual: curl(1)

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
explain
output
{"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
output
  % 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
output
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
explain
output
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
explain
output
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: $?"
output
{
  "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
explain
outputexit 22
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
explain
output
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
explain
output
{
  "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
explain
output
{
  "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
explain
output
{
  "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
explain
output
{
  "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
output
{
  "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 '^[<>]'
explain
output
> 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/
explain
outputexit 7
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 -f or --fail-with-body, and check $?.

  • -s alone also hides error messages, so a failing download looks like an empty result. Use -sS together.

  • -X POST does not add a body and -X GET -d ... still sends a body. Let -d, --json, -G and -I choose the method; use -X only for methods like PUT, PATCH or DELETE.

  • 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: --json needs 7.82 and --fail-with-body needs 7.76. Check curl --version.

Options worth knowing

A selection, described in our own words. For the full list, see curl(1) or curl --help.

OptionWhat it does
-o, --output FILEWrite the body to FILE instead of standard output.
-O, --remote-nameSave to a file named like the last part of the URL.
-L, --locationFollow redirects.
-I, --headSend a HEAD request and show only the headers.
-i, --includePrint the response headers before the body.
-s, --silentHide the progress meter and error messages.
-S, --show-errorWith -s, still print error messages.
-f, --failExit with status 22 and no body on HTTP errors (400 and above).
--fail-with-bodyExit with status 22 on HTTP errors but still print the body.
-v, --verboseTrace the connection, request and response headers on standard error.
-w, --write-out FORMATPrint FORMAT after the transfer, with variables like %{http_code}.
-d, --data DATASend DATA as a POST body; @file reads it from a file.
--data-urlencode DATALike -d, but percent-encode the value.
--data-binary DATASend DATA exactly as given, without stripping newlines.
--json DATAPOST DATA as JSON and set the JSON Content-Type and Accept headers.
-F, --form NAME=VALUESend a multipart form field; NAME=@file uploads a file.
-G, --getPut -d data into the URL query string and use GET.
-H, --header HEADERAdd or replace a request header.
-X, --request METHODUse METHOD instead of the default.
-u, --user USER[:PASSWORD]Send credentials; prompts for the password if omitted.
-A, --user-agent STRINGSet the User-Agent header.
-e, --referer URLSet the Referer header.
-b, --cookie DATA|FILESend cookies from a string or a cookie file.
-c, --cookie-jar FILESave received cookies to FILE.
-k, --insecureSkip TLS certificate verification (testing only).
-C, --continue-at OFFSETResume a transfer; use - to work out the offset from the existing file.
-T, --upload-file FILEUpload FILE (PUT for HTTP).
--retry NUMRetry transient failures up to NUM times.
--retry-delay SECONDSWait this long between retries.
--retry-connrefusedAlso retry when the connection is refused.
-m, --max-time SECONDSGive up if the whole operation takes longer.
--connect-timeout SECONDSGive up if connecting takes longer.
-x, --proxy URLUse the given proxy.
-4, --ipv4Resolve names to IPv4 addresses only.
--compressedAsk for a compressed response and decompress it.

Related