cheat sheet
curl
Transfer data with URLs. Covers HTTP methods, headers, authentication, forms, TLS, cookies, proxies, timeouts, parallel downloads, and a comprehensive recipe collection.
curl — HTTP Client
What it is
curl is a free, open-source command-line tool and library (libcurl) for transferring data with URLs, maintained by Daniel Stenberg and a large open-source community. It supports HTTP, HTTPS, FTP, SFTP, SCP, SMTP, and more than 25 other protocols, making it the standard tool for sending API requests, downloading files, and testing HTTP endpoints from the terminal. Reach for curl when you need fine-grained control over HTTP requests — headers, methods, auth, TLS — in scripts or ad-hoc testing; for recursive mirroring, wget is more convenient.
Basic requests
curl https://example.com # GET, print to stdout
curl -o file.html https://example.com # save to file
curl -O https://example.com/file.tar.gz # save with remote filename
curl -L https://example.com # follow redirects
curl -s https://example.com # silent (no progress)
curl -S https://example.com # show error even with -s
curl -sS https://example.com | jq '.' # common: silent + error visible
curl -v https://example.com # verbose (headers + body)
curl -I https://example.com # HEAD only (headers)
curl --head https://example.com # same as -I
Output:
# curl https://example.com
<!doctype html>
<html>
<title>Example Domain</title>
<body>
<h1>Example Domain</h1>
<p>This domain is for use in illustrative examples...</p>
</body>
</html>
# curl -I https://example.com
HTTP/2 200
content-type: text/html; charset=UTF-8
content-length: 1256
last-modified: Thu, 17 Oct 2019 07:18:26 GMT
etag: "3147526947"
cache-control: max-age=604800
# curl -v https://example.com (abbreviated)
* Trying 93.184.216.34:443...
* Connected to example.com (93.184.216.34) port 443
* TLS 1.3 connection using TLS_AES_256_GCM_SHA384
> GET / HTTP/2
> Host: example.com
< HTTP/2 200
< content-type: text/html; charset=UTF-8
<!doctype html>...
HTTP methods
-X sets the HTTP verb explicitly. GET and HEAD are implied by default; POST is implied when -d or -F is present, so -X POST is technically redundant but common for clarity. Always use -X DELETE or -X PATCH explicitly since curl has no shorthand for them.
curl -X GET https://api.example.com/users
curl -X POST https://api.example.com/users
curl -X PUT https://api.example.com/users/1
curl -X PATCH https://api.example.com/users/1
curl -X DELETE https://api.example.com/users/1
Output:
# curl -X GET https://api.example.com/users
[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]
# curl -X DELETE https://api.example.com/users/1
HTTP/2 204 No Content
Request headers
-H adds or overrides a single request header; repeat it for multiple headers. Use it to set Content-Type, Accept, Authorization, or any custom header the API requires. To remove a header curl would send by default, pass it with an empty value: -H "User-Agent:".
curl -H "Accept: application/json" https://api.example.com
curl -H "Content-Type: application/json" \
-H "Authorization: Bearer TOKEN" https://api.example.com
# Multiple headers
curl -H "X-Request-ID: abc123" \
-H "X-Client-Version: 2.0" \
https://api.example.com
Output:
{
"headers": {
"Accept": "application/json",
"Authorization": "Bearer TOKEN",
"X-Request-Id": "abc123",
"X-Client-Version": "2.0"
}
}
Request body
-d / --data sends a URL-encoded string or raw body and implies POST. Prefix with @ to read from a file (-d @payload.json). For multipart form uploads (file attachments), use -F instead, which sets Content-Type: multipart/form-data automatically.
# JSON body (POST)
curl -X POST https://api.example.com/users \
-H "Content-Type: application/json" \
-d '{"name":"Alice","email":"alice@example.com"}'
# From a file
curl -X POST https://api.example.com/upload \
-H "Content-Type: application/json" \
-d @payload.json
# Form data (application/x-www-form-urlencoded)
curl -X POST https://example.com/login \
-d "username=alice&password=secret"
# Multipart form (file upload)
curl -X POST https://example.com/upload \
-F "file=@report.pdf" \
-F "description=Monthly report"
# Binary stdin
cat image.png | curl -X POST https://api.example.com/image \
-H "Content-Type: image/png" \
--data-binary @-
Output:
# JSON POST response
{"id":42,"name":"Alice","email":"alice@example.com","created_at":"2026-04-26T10:00:00Z"}
# Form login response
HTTP/2 302
Location: /dashboard
Set-Cookie: session=abc123; HttpOnly; Secure
# Multipart upload response
{"filename":"report.pdf","size":204800,"url":"/files/report.pdf"}
JSON shortcut (--json)
--json (curl 7.82+, Apr 2022) sends a JSON body and sets Content-Type: application/json and Accept: application/json in a single flag — equivalent to -X POST -H "Content-Type: application/json" -H "Accept: application/json" -d <data>. Like -d, you can prefix the value with @ to read from a file or @- to read from stdin, and repeating --json concatenates the inputs.
# Equivalent to the long-form POST in the previous section
curl --json '{"name":"Alice","email":"alice@example.com"}' \
https://api.example.com/users
# Read JSON body from file
curl --json @payload.json https://api.example.com/users
# Build JSON from stdin (e.g. via jq)
echo '{"name":"Alice"}' | curl --json @- https://api.example.com/users
# Repeat --json — values are concatenated then sent as one body
curl --json '{"name":"Alice"' --json ',"role":"admin"}' \
https://api.example.com/users
Output:
{"id":42,"name":"Alice","email":"alice@example.com","role":"admin"}
HTTP/2 and HTTP/3
--http2 requests HTTP/2 (default for HTTPS in modern builds); --http2-prior-knowledge skips ALPN and assumes HTTP/2 over cleartext. --http3 uses HTTP/3 over QUIC if curl was built with a QUIC backend (ngtcp2 is non-experimental; quiche and OpenSSL-QUIC are still flagged experimental as of curl 8.20). --http3-only forces HTTP/3 with no fallback. Check curl -V for HTTP2/HTTP3 in the Features line to confirm support in your build.
# Force HTTP/2
curl --http2 https://example.com
# HTTP/2 over cleartext (h2c), skip ALPN
curl --http2-prior-knowledge http://localhost:8080/
# HTTP/3 (requires QUIC-enabled build; will fall back to HTTP/2 on failure)
curl --http3 https://cloudflare-quic.com/
# HTTP/3 only — fail rather than downgrade
curl --http3-only https://cloudflare-quic.com/
# Confirm protocol used
curl -sI -o /dev/null -w "%{http_version}\n" --http3 https://cloudflare-quic.com/
Output:
# curl --http2 https://example.com (verbose excerpt)
* ALPN: server accepted h2
* using HTTP/2
HTTP/2 200
# curl --http3 — protocol confirmation
3
Authentication
-u user:pass sends HTTP Basic auth (base64-encoded, not encrypted — always use HTTPS). For Bearer tokens, pass the Authorization header manually with -H. Client certificate auth (--cert / --key) is a separate mechanism for mutual TLS (mTLS).
# Basic auth
curl -u user:password https://example.com
curl -u user https://example.com # prompt for password
# Bearer token
curl -H "Authorization: Bearer TOKEN" https://api.example.com
# API key in header
curl -H "X-API-Key: my-key" https://api.example.com
# API key in query string
curl "https://api.example.com/data?api_key=my-key"
# Digest auth
curl --digest -u user:pass https://example.com
# NTLM auth — disabled in default builds as of curl 8.20.0 (Apr 2026);
# requires a build with --enable-ntlm or a vendor package that re-enables it
curl --ntlm -u user:pass https://example.com
# AWS SigV4 signed request (curl 7.75+) — signs the request with AWS Signature V4
curl --aws-sigv4 "aws:amz:us-east-1:s3" \
-u "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \
https://my-bucket.s3.us-east-1.amazonaws.com/key
# OAuth 2 bearer token (curl 7.33+) — sets the Authorization: Bearer header
curl --oauth2-bearer "$TOKEN" https://api.example.com
# Client certificate
curl --cert client.pem --key client.key https://api.example.com
Output:
# Basic auth — success
{"user":"alice","role":"admin"}
# Basic auth — failure
HTTP/2 401
WWW-Authenticate: Basic realm="Restricted"
{"error":"Unauthorized"}
TLS / SSL
By default curl verifies the server's certificate against the system CA bundle. -k / --insecure disables verification entirely — only use it for local development or testing, never in production scripts. --cacert supplies a custom CA bundle, useful for internal PKI or self-signed certificates.
curl -k https://self-signed.example.com # skip cert verification
curl --insecure https://dev.local # same as -k
curl --cacert /path/to/ca.pem https://internal.example.com
curl --capath /etc/ssl/certs https://example.com
# Show certificate info
curl -vI https://example.com 2>&1 | grep -A20 "Server certificate"
# Specify TLS version
curl --tlsv1.2 https://example.com
curl --tls-max 1.3 https://example.com
Output:
# curl -vI https://example.com 2>&1 | grep -A20 "Server certificate"
* Server certificate:
* subject: C=US; ST=California; L=Los Angeles; O=Internet Corporation for Assigned Names and Numbers; CN=www.example.org
* start date: Jan 30 00:00:00 2024 GMT
* expire date: Mar 1 23:59:59 2025 GMT
* subjectAltName: host "example.com" matched cert's "example.com"
* issuer: C=US; O=DigiCert Inc; CN=DigiCert Global G2 TLS RSA SHA256 2020 CA1
* SSL certificate verify ok.
Output and response info
curl -w "\nHTTP %{http_code} %{time_total}s\n" -o /dev/null -s https://example.com
# Full timing breakdown
curl -w "\n
DNS: %{time_namelookup}s
Connect: %{time_connect}s
TLS: %{time_appconnect}s
TTFB: %{time_starttransfer}s
Total: %{time_total}s
HTTP: %{http_code}\n" -o /dev/null -sS https://example.com
# Get only the HTTP status code
curl -o /dev/null -s -w "%{http_code}" https://example.com
# Get response headers into a variable
HEADERS=$(curl -sI https://example.com)
Output:
# curl -w "\nHTTP %{http_code} %{time_total}s\n" -o /dev/null -s https://example.com
HTTP 200 0.124032s
# Full timing breakdown
DNS: 0.003102s
Connect: 0.021455s
TLS: 0.057821s
TTFB: 0.123901s
Total: 0.124032s
HTTP: 200
# curl -o /dev/null -s -w "%{http_code}" https://example.com
200
Cookies
-c writes cookies received from the server to a Netscape-format cookie jar file; -b reads and sends cookies from that file on subsequent requests. Use both together to maintain a session across multiple curl calls.
curl -c cookies.txt https://example.com/login -d "user=alice&pass=x"
curl -b cookies.txt https://example.com/dashboard
curl -c cookies.txt -b cookies.txt https://example.com # send + save
# Send a specific cookie
curl -H "Cookie: session=abc123; pref=dark" https://example.com
Output:
# After curl -c cookies.txt https://example.com/login ...
# cat cookies.txt
# Netscape HTTP Cookie File
example.com FALSE / TRUE 1777000000 session abc123
example.com FALSE / FALSE 0 pref dark
# curl -b cookies.txt https://example.com/dashboard
{"user":"alice","dashboard":true}
Redirects
By default curl does not follow HTTP 3xx redirects — it just prints the redirect response. -L enables following, which is almost always what you want for real URLs. Curl preserves the method for 307/308 redirects but may switch to GET on 301/302.
curl -L https://short.url/abc # follow redirects
curl --max-redirs 5 -L https://example.com # max 5 redirects
curl -D - https://example.com # dump headers to stdout
Output:
# curl -D - https://example.com
HTTP/2 200
content-type: text/html; charset=UTF-8
content-length: 1256
cache-control: max-age=604800
etag: "3147526947"
<!doctype html>
<html>...
# curl -L https://short.url/abc (redirect followed silently)
<!doctype html>... (final destination content)
Proxy
-x / --proxy routes the request through an HTTP, HTTPS, or SOCKS proxy. Curl also honors the http_proxy, https_proxy, and no_proxy environment variables, so you often don't need the flag if the proxy is already configured in the shell environment.
curl -x http://proxy:8080 https://example.com
curl --proxy socks5://proxy:1080 https://example.com
curl --noproxy localhost,192.168.0.0/16 https://example.com
export http_proxy=http://proxy:8080 # environment variable
Output:
# curl -x http://proxy:8080 https://example.com (response via proxy)
<!doctype html>
<html>
<title>Example Domain</title>
...
Timeouts and retries
--connect-timeout limits only the TCP handshake; --max-time caps the entire operation including transfer. --retry retries on transient network errors (not on HTTP 4xx/5xx by default — add --retry-all-errors to retry on those too).
curl --connect-timeout 5 https://example.com # connection timeout (s)
curl --max-time 30 https://example.com # total operation timeout
curl --retry 3 https://example.com # retry on transient error
curl --retry 3 --retry-delay 2 https://example.com
curl --retry 3 --retry-all-errors https://example.com
Output:
# On connection timeout:
curl: (28) Connection timed out after 5001 milliseconds
# On --retry 3 with transient failure (retries logged to stderr):
Warning: Transient problem: HTTP error Will retry in 2 seconds. 3 retries left.
Warning: Transient problem: HTTP error Will retry in 2 seconds. 2 retries left.
Warning: Transient problem: HTTP error Will retry in 2 seconds. 1 retries left.
curl: (22) The requested URL returned error: 503
Download management
-O saves the response using the remote filename; -o specifies the local name explicitly. -C - resumes an interrupted download by sending a Range header based on the existing file size. The --parallel flag (curl 7.66+) downloads multiple URLs concurrently in a single invocation.
# Resume interrupted download
curl -C - -O https://example.com/bigfile.tar.gz
# Speed limit (curl 8.19+ accepts fractional units, e.g. 1.5M)
curl --limit-rate 500k -O https://example.com/file.tar.gz
curl --limit-rate 1.5M -O https://example.com/file.tar.gz
# Progress bar
curl -# -O https://example.com/file.tar.gz
# Parallel downloads (curl 7.66+)
curl --parallel --parallel-max 5 -O -O -O \
https://example.com/file1.tar.gz \
https://example.com/file2.tar.gz \
https://example.com/file3.tar.gz
Output:
# curl -C - -O https://example.com/bigfile.tar.gz (resume)
** Resuming transfer from byte position 52428800
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 500M 100 500M 0 0 4500k 0 0:01:53 0:01:53 --:--:-- 4821k
# curl -# -O https://example.com/file.tar.gz (progress bar)
######################################################################## 100.0%
# curl --limit-rate 500k -O ...
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
42 100M 42 42.3M 0 0 500k 0 0:03:24 0:01:26 0:01:58 500k
Configuration
curl reads a default config file at startup — ~/.curlrc on Unix/macOS and %USERPROFILE%\_curlrc on Windows (the underscore prefix is the Windows convention). Every line is a long-form option without the leading --; lines starting with # are comments. -K <file> / --config <file> reads an alternate config (use -K - to read options from stdin), and -q / --disable skips the default config for a single invocation when you need a clean environment.
# ~/.curlrc — applied to every curl invocation
# Always follow redirects
--location
# Silent by default
--silent
--show-error
# Default headers
--header "Accept: application/json"
# Timeout
--max-time 30
--connect-timeout 10
# Retry
--retry 3
# Use an alternate config file for one call
curl -K ./prod.curlrc https://api.example.com
# Pipe options from stdin
printf -- "--header 'X-Trace: abc'\n--retry 5\n" | curl -K - https://api.example.com
# Skip ~/.curlrc entirely for this call
curl -q https://example.com
Output: (none — exits 0 on success)
Practical API recipes
# Health check
check() { curl -fsS "$1" > /dev/null && echo "UP" || echo "DOWN"; }
check https://api.example.com/health
# Paginate GitHub API
for page in $(seq 1 5); do
curl -sS "https://api.github.com/users/USER/repos?per_page=100&page=$page"
done | jq -r '.[].full_name'
# POST JSON and extract field from response
TOKEN=$(curl -sS -X POST https://auth.example.com/token \
-H "Content-Type: application/json" \
-d '{"client_id":"ID","client_secret":"SECRET"}' \
| jq -r '.access_token')
# Check HTTP status in a script
STATUS=$(curl -o /dev/null -sS -w "%{http_code}" https://api.example.com)
[ "$STATUS" = "200" ] || { echo "API returned $STATUS"; exit 1; }
# Stream NDJSON (newline-delimited JSON)
curl -sS --no-buffer https://api.example.com/stream \
| while IFS= read -r line; do echo "$line" | jq '.event'; done
# Send multipart with metadata
curl -X POST https://api.example.com/upload \
-F 'metadata={"filename":"report.pdf"};type=application/json' \
-F 'file=@report.pdf;type=application/pdf'
Output:
# check https://api.example.com/health
UP
# Paginate GitHub API
user/repo-one
user/repo-two
user/repo-three
# POST JSON and extract token
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
# Status check (passing)
(exit 0, no output)
# Status check (failing)
API returned 503
# Stream NDJSON
"ping"
"message"
"close"
# Multipart upload
{"filename":"report.pdf","size":204800,"url":"/files/report.pdf"}
--data-urlencode
# URL-encode a value (avoids manual %xx encoding)
curl -G --data-urlencode "q=hello world" https://httpbin.org/get
curl -X POST --data-urlencode "message=hello world" https://httpbin.org/post
Output:
{
"args": {
"q": "hello world"
},
"url": "https://httpbin.org/get?q=hello+world"
}
DNS override and Unix sockets
# Override DNS resolution (bypass DNS for testing)
curl --resolve example.com:443:93.184.216.34 https://example.com/
# Connect via Unix domain socket
curl --unix-socket /var/run/docker.sock http://localhost/version
# Connect via abstract socket (Linux)
curl --abstract-unix-socket myservice http://localhost/health
Output:
<!doctype html>
<html>
<title>Example Domain</title>
...
# Docker socket response
{"Platform":{"Name":"Docker Engine - Community"},"Version":"26.1.0","ApiVersion":"1.45","Os":"linux","Arch":"amd64"}
Multi-request with --next
# GET then POST in one invocation (resets most options between requests)
curl https://httpbin.org/get \
--next \
-X POST -d '{"ok":true}' https://httpbin.org/post
# GET two URLs sequentially, each with its own -o
curl -o a.html https://example.com/ \
--next \
-o b.html https://example.net/
Output:
# First request output (GET):
{"args": {}, "url": "https://httpbin.org/get"}
# Second request output (POST):
{"data": "{\"ok\":true}", "url": "https://httpbin.org/post"}
Config file (.curlrc)
# ~/.curlrc — options applied to every curl invocation
# Each option on its own line; long names without leading --
Output: (none — exits 0 on success)
# ~/.curlrc
silent
show-error
location
max-time = 30
retry = 3
retry-delay = 2
user-agent = "myclient/1.0"
# Override .curlrc for a single invocation
curl -q https://example.com/ # -q disables .curlrc
Output: (none — exits 0 on success)
--write-out variable reference
# Time the DNS lookup, TCP connect, TLS handshake, and total
curl -s -o /dev/null \
-w "dns:%{time_namelookup} tcp:%{time_connect} tls:%{time_appconnect} ttfb:%{time_starttransfer} total:%{time_total}\n" \
https://example.com/
Output:
dns:0.003 tcp:0.021 tls:0.057 ttfb:0.124 total:0.124
# Full set of useful --write-out variables
curl -s -o /dev/null -w "\
status: %{response_code}\n\
content-type: %{content_type}\n\
size-download: %{size_download} bytes\n\
speed-download: %{speed_download} B/s\n\
time-total: %{time_total}s\n\
url-effective: %{url_effective}\n\
num-redirects: %{num_redirects}\n\
ssl-verify-result: %{ssl_verify_result}\n" \
https://example.com/
Output:
status: 200
content-type: text/html
size-download: 1256 bytes
speed-download: 45231 B/s
time-total: 0.124s
url-effective: https://example.com/
num-redirects: 0
ssl-verify-result: 0
Streaming and buffering
# Disable buffering for streaming responses (Server-Sent Events, chunked)
curl -N https://api.example.com/stream
# Stream a large file to stdout without buffering
curl -s --no-buffer https://example.com/large.ndjson | jq -c '.'
Output:
data: {"event":"ping","time":1712000000}
data: {"event":"message","content":"hello"}
data: {"event":"close"}
--fail-with-body and error handling
# Exit non-zero on HTTP ≥400, but still print the response body
curl --fail-with-body https://httpbin.org/status/404
# Classic --fail (exits non-zero, prints nothing on error)
curl --fail https://httpbin.org/status/500
# Check exit code
curl --fail-with-body -s https://api.example.com/data || echo "Request failed"
Output:
{
"status": 404,
"error": "Not Found"
}
mTLS with certificates
# Client certificate + key (PEM format)
curl --cert client.crt --key client.key https://api.example.com/
# Client certificate bundle (cert + key in one PEM file)
curl --cert client-bundle.pem https://api.example.com/
# PKCS#12 bundle (.p12 / .pfx)
curl --cert-type P12 --cert client.p12:password https://api.example.com/
# CA certificate for server verification
curl --cacert /path/to/ca.crt https://internal.example.com/
# Skip server cert verification (TESTING ONLY — insecure)
curl -k https://self-signed.badssl.com/
Output:
{"authenticated": true, "user": "client-cn"}
-G to convert body to query string
# Append POST body params as GET query string instead
curl -G -d "page=1" -d "limit=20" https://httpbin.org/get
# Equivalent to: curl https://httpbin.org/get?page=1&limit=20
Output:
{
"args": {
"limit": "20",
"page": "1"
},
"url": "https://httpbin.org/get?page=1&limit=20"
}
Command-line variables (--variable)
--variable (curl 7.83+, with expansion improvements through 8.3+) defines a named variable that you reference elsewhere on the same command line as {{name}} (with optional functions like {{name:url}}, {{name:b64}}, {{name:json}}, {{name:trim}}). Source a variable from a file with --variable name@file or from an environment variable with --variable %ENV_VAR. Useful for templating headers, URLs, and bodies without shell quoting.
# Inline definition + expansion
curl --variable host=api.example.com \
--variable user=alice \
-H "X-User: {{user}}" \
"https://{{host}}/users/{{user}}"
# From environment variable, JSON-escape it for use in a JSON body
curl --variable %TOKEN \
--json '{"token":"{{TOKEN:json}}"}' \
https://api.example.com/auth
# From a file, base64-encode it
curl --variable cred@./secret.txt \
-H "Authorization: Basic {{cred:b64}}" \
https://api.example.com
Output:
{"user":"alice","authenticated":true}
--trace-ascii for debugging
# Dump full protocol trace (ASCII) to stderr
curl --trace-ascii - https://httpbin.org/get 2>&1 | head -30
# Dump to a file instead
curl --trace-ascii trace.txt https://httpbin.org/get
Output:
== Info: Trying 54.147.56.198:443...
== Info: Connected to httpbin.org (54.147.56.198) port 443
== Info: TLS 1.3 connection using TLS_AES_256_GCM_SHA384
=> Send header, 86 bytes
0000: GET /get HTTP/1.1
0014: Host: httpbin.org
…
<= Recv header, 17 bytes
0000: HTTP/1.1 200 OK
…
Use
curl --write-out '%{http_code}' --fail(-f) together:-fmakes curl exit non-zero on HTTP error (4xx/5xx), and-w '%{http_code}'still lets you capture the code. In CI,-fsSis the canonical quiet-but-reliable combination.
Alternatives
curl is the lingua franca, but two modern reimplementations target the API-testing use case with friendlier defaults:
- HTTPie (
http/https) — Python-based, JSON-first (http POST api.example.com name=alicebuilds and sends a JSON body), colorized output, sensible defaults for headers and follow-redirects. - xh — single-binary Rust rewrite of HTTPie with near-identical syntax, much faster startup, and HTTP/2 + HTTP/3 support out of the box. Often preferred for scripting where you want HTTPie ergonomics without the Python runtime.
Both speak the same key=value (form) / key:=value (JSON) / key:value (header) shorthand and accept full URLs. Keep curl for libraries that copy-paste curl ... examples, mTLS, exotic protocols (SFTP, SMTP, MQTT), and anywhere a single-binary universal default is preferable.