curl Quick Reference
Talk to any HTTP API from the terminal — headers, bodies, auth, downloads, and the flags that make debugging painless.
curl sends a request and prints the response body to stdout, nothing more unless you ask. Every flag below adjusts that: -i, -v and -w show more of the exchange, -s shows less, -o sends the body elsewhere, and -d and -H shape what goes out. Two defaults catch people: curl does not follow redirects without -L, and it exits 0 on a 404 or 500 unless you add -f, because the transfer itself worked.
Short flags bundle (-fsSL). Run curl --version before trusting a newer flag: --json needs 7.82 or later.
Requests & bodies
- curl URL
- GET is the default; quote any URL containing
&or?so the shell leaves it alone - -X PUT · -X DELETE
- set the method (also
PATCH);-dalready implies POST, so-X POSTis redundant there - -d 'a=1&b=2'
- send a body as POST, form-encoded by default — set a JSON
Content-Typeyourself - --json '{"a":1}'
- POST JSON with
Content-TypeandAcceptset (7.82+); older curl:-Hplus-d - -d @body.json
- body from a file,
@-from stdin; strips newlines —--data-binarysends bytes as-is - --data-binary @file
- send the file byte-for-byte, newlines kept — for NDJSON bulk APIs and signed payloads
- -F file=@photo.png
- multipart upload; add text fields with
-F "name=value" - -T file.zip URL
- upload a file with PUT — e.g. to a presigned S3 URL
- -G --data-urlencode
- URL-encode
"q=a b"into the query string of a GET
Headers & auth
- -H "Name: value"
- add a header (repeatable);
-H "Accept:"removes a default,-H @filereads them from a file - --oauth2-bearer "$TOKEN"
- sends
Authorization: Bearer …— same as writing the header by hand - -u user:pass
- Basic auth;
-u useralone prompts for the password, keeping it out of history - -n
- take credentials from
~/.netrcso no secret appears on the command line - -b "session=abc"
- send cookies;
-b jar.txtreads a cookie file - -c jar.txt
- save cookies the server sets;
-b jar.txt -c jar.txtkeeps a session going - -A "my-agent/1.0"
- set the User-Agent; some APIs reject requests without a descriptive one
- --aws-sigv4
- sign AWS-style, e.g.
"aws:amz:us-east-1:s3"with-u KEY:SECRET(7.75+)
Inspect & debug
- -i
- show response headers, then the body
- -I
- HEAD request, headers only; some servers answer HEAD differently from GET
- -v
- request and response headers plus the TLS handshake, on stderr
- --trace-ascii -
- everything on the wire, request body included; add
--trace-timefor timestamps - -sD - -o /dev/null
- print a real GET’s headers and discard the body — for when HEAD lies
- -w '%{http_code}\n'
- print the status after the transfer; with
-s -o /dev/nullit is the only output - -w '%{time_total}'
- also
time_namelookup,time_connect,time_appconnect(TLS),time_starttransfer(TTFB) - -w @format.txt
- read the write-out template from a file — a reusable timing report
- -w '%{json}'
- every write-out variable as one JSON object (7.70+); pipe it to
jq - -sS
-ssilences progress and errors; adding-Sbrings error messages back
Downloads & output
- -o out.zip · -O
- save under a name you choose · under the URL’s own file name
- -OJ
- use the file name from the server’s
Content-Dispositionheader - -L
- follow redirects; POST becomes GET after 301/302/303, and credentials stay with the first host
- -C - -O URL
- resume an interrupted download from where the partial file stops
- -r 0-1023
- fetch a byte range; the server answers
206if it supports ranges - --compressed
- ask for gzip or brotli and decompress automatically, as browsers do
- --create-dirs -o a/b/f
- create missing directories for the output path
- -Z URL1 URL2
- transfer in parallel (7.66+); cap concurrency with
--parallel-max - --limit-rate 1M
- throttle bandwidth — simulate slow clients or go easy on a server
Scripts: fail, retry, time out
- -f
- HTTP 400+ exits with code 22 and no body;
--fail-with-bodykeeps the body (7.76+) - curl -fsSL URL
- the install-script idiom: fail on errors, quiet, still show errors, follow redirects
- --retry 3
- retry timeouts, 408, 429 and 5xx with doubling backoff;
--retry-delayfixes the wait - --retry-all-errors
- retry on any failure, e.g. connection refused (7.71+); safe only for idempotent calls
- -m 30
- cap the whole transfer at 30 seconds — without it a stuck request can hang forever
- --connect-timeout 5
- cap only the connection phase; pair it with
-m - --retry-max-time 60
- stop retrying once 60 seconds have passed in total
TLS, DNS & proxies
- -k
- skip certificate verification — for debugging only, never in a production script
- --cacert ca.pem
- trust a private CA instead of turning verification off
- --cert c.pem --key k.pem
- present a client certificate for mutual TLS
- --resolve h:443:1.2.3.4
- pin a host to an IP for one request; SNI and certificate checks still use the name
- -x http://proxy:3128
- route through a proxy; curl also reads
HTTPS_PROXYandNO_PROXY - --http1.1 · --http2
- force a protocol version;
--http3needs a curl built with HTTP/3 - -4 · -6
- force IPv4 or IPv6 — isolates bugs that only appear on one stack
- --unix-socket PATH
- speak HTTP over a Unix socket, e.g. Docker’s
/var/run/docker.sock
Recipes
Call a JSON API with a token and pretty-print the result
TOKEN=$(cat ~/.config/myapi/token)
curl -fsS --json '{"name":"demo"}' -H "Authorization: Bearer $TOKEN" https://api.example.com/v1/items | jq .--json sets both JSON headers and implies POST (7.82+); -f turns a 4xx or 5xx into a non-zero exit, so a CI step fails instead of piping an error page into jq.
Break down where a slow request spends its time
curl -s -o /dev/null -w 'dns %{time_namelookup}\nconnect %{time_connect}\ntls %{time_appconnect}\nttfb %{time_starttransfer}\ntotal %{time_total}\n' https://example.comEach figure is seconds since the start, so TTFB minus TLS is roughly the server’s think time. Large connect or TLS numbers point at the network, not the app.
Test a new server before DNS points at it
curl -sv -o /dev/null --resolve app.example.com:443:203.0.113.10 https://app.example.com/healthz--resolve sends the request to the new IP but keeps SNI and certificate checks for the real hostname, which curling the bare IP cannot do.
Download a release reliably in a script
curl -fL --retry 5 --connect-timeout 10 -m 600 -o app.tar.gz https://example.com/app.tar.gz
echo "EXPECTED_SHA256 app.tar.gz" | sha256sum -c -Retries absorb transient network errors and 5xx responses, and the checksum proves the bytes are right. On macOS use shasum -a 256 -c - instead.
Check a list of URLs and print their status codes
while read -r url; do
code=$(curl -s -o /dev/null -m 10 -w '%{http_code}' "$url")
echo "$code $url"
done < urls.txtA 000 means no HTTP response arrived at all: a DNS, connection or timeout failure.
Questions about curl Cheat Sheet
How do I send a POST request with JSON using curl?
On curl 7.82 or newer, pass the body to --json, for example curl --json '{"name":"demo"}' https://api.example.com/items. It sets Content-Type and Accept to application/json and implies POST. On older versions, combine -H 'Content-Type: application/json' with -d and the body. Keep the JSON in single quotes so the shell leaves it alone, or store it in a file and pass @body.json. Note that -d strips newlines when it reads a file, which is harmless for JSON but breaks NDJSON, so use --data-binary when line breaks matter.
Why does curl exit with code 0 on a 404 or 500?
Because from curl’s point of view the transfer succeeded: it connected, sent the request and received a complete response. HTTP status codes are the server’s business. Add -f (or --fail) and curl exits with code 22 for any status of 400 or above and prints no body, which is what scripts and CI steps need. If you want the error body too, use --fail-with-body on curl 7.76 or newer. To branch on the exact status, capture it with -w '%{http_code}' and compare it in the shell.
How do I see the request and response headers with curl?
Use -v to print the whole exchange on stderr: the connection and TLS handshake, every request header curl sends and every response header. Use -i to put the response headers in the normal output, ahead of the body. -I sends a HEAD request and prints only headers, but some servers answer HEAD differently from GET, so -s -D - -o /dev/null is the reliable way to see a real GET’s headers. When you also need the request body and the exact bytes, --trace-ascii - writes everything on the wire to stdout.
What is the difference between curl -d and -F?
Option -d sends the data as the raw request body, by default labeled application/x-www-form-urlencoded, the format of a plain HTML form; it is also how you send JSON once you set the content type. Option -F builds a multipart/form-data body, the format browsers use for file uploads: each -F adds one part, and name=@file attaches a file with its own filename and type. Use -d for APIs and simple forms, and -F when the server expects an upload field. Both imply POST unless you change the method.