Use curl Like a Pro
Master curl beyond basic GET requests: inspect and send headers, authenticate with tokens, POST JSON, upload files, manage cookies, and debug with -v.
Before you start
- ▸curl installed (version 7.76 or later recommended)
- ▸Basic familiarity with a terminal and shell scripting
- ▸Network access to a test endpoint such as httpbin.org
curl ships on virtually every Linux system and does far more than fetch a URL. Once you move past simple GET requests, you unlock a precise HTTP client you can script, debug, and integrate into pipelines. This guide covers the options you'll reach for daily: inspecting headers, authenticating, posting JSON, uploading files, managing cookies, and tracing exactly what's happening on the wire.
Prerequisites and Version Check
Most distros ship a recent curl. Confirm yours:
curl --version
Output will start with something like curl 8.5.0 (x86_64-pc-linux-gnu) and list supported protocols and features. You need at least 7.76 for --fail-with-body; 7.55+ for --data-raw. If your distro lags, install from the official repo or use a container.
Working with Headers
Viewing Response Headers
Use -I (HEAD request) to fetch only headers, or -i to include headers above the body in a normal GET:
curl -I https://example.com
curl -i https://example.com
To dump headers to a file while still printing the body normally, use -D:
curl -D headers.txt https://example.com -o body.html
Sending Custom Request Headers
Pass each header with -H. Stack multiple -H flags as needed:
curl -H "Accept: application/json" \
-H "X-Request-ID: abc123" \
https://api.example.com/status
To remove a default header curl adds (like Host or User-Agent), pass it with an empty value:
curl -H "User-Agent:" https://example.com
Authentication
HTTP Basic Auth
Use -u user:password. Avoid putting passwords in shell history by omitting the password — curl will prompt:
curl -u alice https://api.example.com/private
For scripting where the password must be non-interactive, store it in a .netrc file (chmod 600 ~/.netrc) and use --netrc:
curl --netrc https://api.example.com/private
The ~/.netrc format is: machine api.example.com login alice password s3cr3t
Bearer Token Auth
Pass the Authorization header directly:
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
curl -H "Authorization: Bearer $TOKEN" https://api.example.com/me
Store tokens in environment variables or a secrets manager; never hardcode them in scripts committed to version control.
Posting JSON
Set the content type and provide the payload with -d (or --data-raw to prevent @ being treated as a filename). Always pair with -X POST or just rely on curl inferring POST when you provide -d:
curl -X POST https://api.example.com/users \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"name": "Alice", "role": "admin"}'
For longer payloads, store JSON in a file and reference it with @:
curl -X POST https://api.example.com/users \
-H "Content-Type: application/json" \
-d @payload.json
To avoid quoting headaches with shell variables inside JSON, use --data-raw or build the JSON with jq first:
PAYLOAD=$(jq -n --arg name "Alice" '{name: $name, role: "admin"}')
curl -X POST https://api.example.com/users \
-H "Content-Type: application/json" \
-d "$PAYLOAD"
File Uploads
Multipart Form Upload
Use -F to send a multipart/form-data request, the same format a browser uses for file input fields:
curl -X POST https://upload.example.com/files \
-F "file=@/path/to/report.pdf" \
-F "description=Monthly report"
Override the detected MIME type or set a remote filename:
curl -X POST https://upload.example.com/files \
-F "[email protected];type=application/pdf;filename=march-report.pdf"
Raw Binary Upload (PUT)
Some APIs expect the file body directly without multipart wrapping:
curl -X PUT https://storage.example.com/objects/image.png \
-H "Content-Type: image/png" \
--data-binary @photo.png
Managing Cookies
Cookies matter for session-based APIs and web scraping. Use -c to save cookies to a file and -b to send them back:
# Log in and save the session cookie
curl -c cookies.txt -X POST https://app.example.com/login \
-d "username=alice&password=hunter2"
# Make an authenticated request using the saved cookie
curl -b cookies.txt https://app.example.com/dashboard
Use both flags together to keep updating the cookie jar across redirects in one command:
curl -b cookies.txt -c cookies.txt -L https://app.example.com/profile
Send a specific cookie inline without a file:
curl -b "session=abc123; theme=dark" https://app.example.com/
Debugging with -v and --trace
Verbose Mode
-v prints the full request/response exchange to stderr — request headers, response headers, TLS handshake details, and redirect chain. Lines starting with > are sent; < are received; * are curl status messages:
curl -v https://api.example.com/status
Redirect verbose output to a file while still seeing the body on your terminal:
curl -v https://api.example.com/status 2>debug.log
Full Hex Trace
For low-level inspection of every byte (useful for binary protocols or TLS debugging):
curl --trace trace.bin https://api.example.com/status
curl --trace-ascii trace.txt https://api.example.com/status
--trace-ascii is human-readable and usually enough. Combine with --trace-time to add microsecond timestamps to each line.
Timing Breakdown
Profile where time is spent in a request using -w (write-out):
curl -o /dev/null -s -w \
"DNS: %{time_namelookup}s\nConnect: %{time_connect}s\nTLS: %{time_appconnect}s\nTTFB: %{time_starttransfer}s\nTotal: %{time_total}s\n" \
https://api.example.com/status
This outputs nothing about the body, only timing — useful for latency benchmarking in scripts.
Useful Flags to Know
- -L — follow redirects (301, 302, 307, 308)
- -s — silent mode, suppresses progress meter and error messages; pair with -S to still show errors
- -o file — write body to file instead of stdout
- -O — write body to a file named after the remote filename
- --fail — exit non-zero on HTTP 4xx/5xx (useful in scripts); use --fail-with-body (curl 7.76+) to also print the error response
- --max-time 10 — abort if the whole transfer takes over 10 seconds
- --retry 3 --retry-delay 2 — retry on transient failures
- -k / --insecure — skip TLS certificate verification. Never use in production; only for local dev with self-signed certs.
Verification
Test your understanding with a real public echo API. httpbin.org reflects back exactly what you send:
curl -s -X POST https://httpbin.org/post \
-H "Content-Type: application/json" \
-H "X-Custom: test" \
-d '{"hello": "world"}' | python3 -m json.tool
The JSON response will include your headers, body, and origin IP. Confirm json.hello equals world and your X-Custom header appears under headers.
Troubleshooting
- Certificate errors: Run
curl -vand look for TLS handshake failures. Update your CA bundle (sudo update-ca-certificateson Debian/Ubuntu;sudo update-ca-truston Fedora/RHEL) before resorting to-k. - Empty responses or 000 status: Usually a DNS or connection failure. Add
-vto see where it dies. Check--resolveto force a specific IP. - Unexpected 400 on JSON POST: Confirm the Content-Type header is present and the JSON is valid (
echo '...' | jq .). Some APIs also requireAccept: application/json. - Shell quoting issues: Single quotes work best for JSON in bash. If you need variable interpolation inside JSON, build with
jqinstead of string concatenation. - Proxy environments: curl respects
http_proxy,https_proxy, andno_proxyenvironment variables. Export them or use-x proxy:portexplicitly.
Frequently asked questions
- How do I suppress the progress bar but still see errors?
- Use -s (silent) combined with -S (show errors). The combination -sS suppresses the meter but prints any curl-level errors to stderr, which is ideal for scripts.
- What's the difference between -d and --data-raw?
- With -d, a value starting with @ is treated as a filename to read from. --data-raw sends the string literally, so @ has no special meaning. Use --data-raw when your data might start with @ or when you want to be explicit.
- How do I make curl exit with a non-zero code on HTTP errors?
- Use --fail for silent failure, or --fail-with-body (curl 7.76+) to also print the server's error response body before exiting non-zero. This is essential for reliable use in shell scripts and CI pipelines.
- Can curl follow redirects automatically?
- Yes, add the -L flag. By default curl stops at the first 3xx response. Combine with --max-redirs 5 to cap the redirect chain and prevent infinite redirect loops.
- How do I test an API running locally with a self-signed certificate?
- Use -k (--insecure) to skip certificate validation. This is acceptable only for local development; never use it against production endpoints because it disables all TLS protection.
Related guides
Bash Arrays and Associative Arrays
Master bash indexed and associative arrays: declaration, element access, looping, mapfile, namerefs, and practical patterns for real scripting work.
Bash Functions and Variable Scoping
Master Bash function scoping with local variables, source-based libraries, correct use of return codes, and array passing techniques including namerefs.
Bash Loops: for, while and until
Learn all three Bash loop types — for, while, and until — with practical, copy-paste examples covering file iteration, counting, polling, and safe line reading.
Bash Scripting for Beginners
Learn Bash scripting from scratch: shebang lines, variables, conditionals, loops, and arguments, plus a real backup script to tie it all together.