Getting Started
Basic Requests
curl makes HTTP requests from the command line. By default it does GET. -v shows verbose output, -o saves to a named file, -O keeps the remote filename, -L follows redirects, -I fetches only headers (HEAD request).
# GET request (default)
curl https://example.com
# with verbose output
curl -v https://example.com
# save output to file
curl -o output.html https://example.com
curl -O https://example.com/file.zip # keep remote filename
# follow redirects
curl -L https://example.com/redirect
# show only headers
curl -I https://example.com
curl --head https://example.comOutput & Save
-o writes the body to a named file; -O uses the remote filename. --output-dir sets the destination directory. -s (silent) suppresses the progress meter and errors — pair with -S to still show errors. Use -D to dump headers to a file while discarding the body with -o /dev/null.
# print body to stdout (default)
curl https://example.com
# save body to file
curl -o page.html https://example.com
# save with remote filename
curl -O https://example.com/file.zip
# save to specific dir with remote name
curl -O --output-dir /tmp https://example.com/file.zip
# append to file
curl https://example.com >> log.txt
# discard body, keep headers
curl -o /dev/null -D headers.txt https://example.com
# silent (no progress, no errors)
curl -s https://example.comVerbose & Headers
-v (verbose) prints the full conversation: '>' is the request, '<' is the response, '*' is curl info — all sent to stderr so stdout stays clean for the body. -I sends a HEAD request and prints headers. -D dumps headers to a file or '-' for stdout; combine with -o /dev/null to inspect headers without saving the body.
# verbose: shows request + response, sent to stderr
curl -v https://example.com
# > lines are the request, < lines are the response
# * lines are curl's internal info
# headers only (HEAD request)
curl -I https://example.com
# dump response headers to file
curl -D headers.txt -o body.html https://example.com
# dump headers to stdout, body to file
curl -D - -o body.html https://example.com
# show only response headers (no HEAD)
curl -s -o /dev/null -D - https://example.comFollowing Redirects
-L follows HTTP 3xx redirects; by default curl follows up to 50 hops (use --max-redirs to cap). RFC 301/302 typically converts POST to GET — use --post301/--post302 to preserve the method. By default credentials are not sent to redirected hosts; --location-trusted sends them everywhere (security risk).
# follow 3xx redirects
curl -L https://example.com/old
# limit number of redirects
curl -L --max-redirs 5 https://example.com
# treat redirects as POST (default resends as GET)
curl -L --post301 --post302 https://example.com
# show each hop in a redirect chain
curl -L -v https://short.url/abc 2>&1 | grep -i location
# follow only http/https redirects
curl -L --proto-redir http,https https://example.com
# send credentials only on first request
curl -L --location-trusted -u user:pass https://example.comURL Syntax & Globbing
curl expands URL 'globbing' patterns: [1-10] ranges, {a,b,c} lists, with an optional :step. Each expansion is a separate request. Use -g (--globoff) to treat brackets and braces literally — important for URLs containing JSON arrays or IPv6 addresses like http://[::1]:8080/.
# range glob: requests file1, file2, file3
curl -O https://example.com/file[1-3].zip
# range with step
curl -O https://example.com/file[1-10:2].zip # 1,3,5,7,9
# list glob: a, b, c
curl -O https://example.com/{a,b,c}.zip
# combined globs
curl -O https://example.com/{v1,v2}/file[1-2].txt
# output each URL as it is fetched
curl -O https://example.com/file[1-3].zip --write-out "%{url} -> %{filename}\n"
# disable globbing (literal brackets/braces)
curl -g "https://example.com/file[1].zip"Help & Version
--version shows the curl version, supported protocols (HTTP, HTTPS, FTP, etc.), and compiled-in features (HTTP2, SSL, brotli). --help all lists every option — useful when you remember part of a flag name. Features depend on how curl was built (e.g., HTTP/3 needs a special build).
# version and supported features
curl --version
# short help (one-line per option)
curl --help
# long help (all options with details)
curl --help all
# search help for a keyword
curl --help all | grep -i cookie
# list supported protocols
curl --version | head -1
# check if a feature is built in
curl --version | grep -i http2HTTP Methods
GET Requests
GET is the default method. Always quote URLs containing & or ? so the shell doesn't background or expand them. -X explicitly sets the method but is usually unnecessary for GET. For APIs, set Accept to negotiate the response format.
# simple GET (default method)
curl https://api.example.com/data
# GET with query string
curl "https://api.example.com/search?q=curl&page=2"
# explicit method
curl -X GET https://example.com
# GET with headers
curl -H "Accept: application/json" https://api.example.com/data
# GET with auth
curl -u user:pass https://api.example.com/secretPOST Requests
Sending -d automatically switches the method to POST, so -X POST is redundant (but harmless and clarifying). To POST a file, use -d @filename (the @ reads the file). The default Content-Type for -d is application/x-www-form-urlencoded; override with -H for JSON.
# POST with form data (application/x-www-form-urlencoded)
curl -X POST -d "name=Alice&age=30" https://api.example.com/users
# -d implies POST, so -X is optional
curl -d "name=Alice" https://api.example.com/users
# POST with no body
curl -X POST https://api.example.com/trigger
# POST JSON
curl -X POST -H "Content-Type: application/json" \
-d '{"name":"Alice"}' https://api.example.com/users
# POST with data from a file
curl -X POST -d @form.txt https://api.example.com/usersPUT & PATCH
PUT replaces a whole resource; PATCH applies a partial update. Both require -X because curl has no -d shortcut for them. -T (upload-file) streams a file as the request body and is ideal for PUT uploads. Use --data-binary instead of -d to avoid stripping newlines when sending raw files.
# PUT (replace entire resource)
curl -X PUT -H "Content-Type: application/json" \
-d '{"name":"Alice","age":31}' \
https://api.example.com/users/1
# PATCH (partial update)
curl -X PATCH -H "Content-Type: application/json" \
-d '{"age":32}' \
https://api.example.com/users/1
# PUT a file as body
curl -X PUT --data-binary @config.json \
-H "Content-Type: application/json" \
https://api.example.com/config
# PUT with upload file
curl -X PUT -T localfile.txt https://example.com/remote.txtDELETE
DELETE removes a resource. Most APIs require authentication, so include -u or an Authorization header. Some APIs accept a body (reason, cascade flags) — add -d and a Content-Type header. Use -w '%{http_code}' to check the result: 204 (No Content) and 200 are typical successes.
# delete a resource
curl -X DELETE https://api.example.com/users/1
# delete with auth
curl -X DELETE -H "Authorization: Bearer TOKEN" \
https://api.example.com/users/1
# delete with confirmation check
curl -X DELETE -w "%{http_code}" -o /dev/null -s \
https://api.example.com/users/1
# delete with request body (some APIs require it)
curl -X DELETE -H "Content-Type: application/json" \
-d '{"reason":"spam"}' \
https://api.example.com/users/1HEAD & OPTIONS
-I sends a HEAD request (headers only, no body) — useful for checking size (Content-Length), type (Content-Type), and last-modified without downloading. OPTIONS reveals allowed methods (the Allow header) and is used for CORS preflight by browsers. Add Origin and Access-Control-Request-Method headers to simulate a real preflight.
# HEAD: headers only, no body
curl -I https://example.com
curl --head https://example.com
# OPTIONS: discover allowed methods
curl -X OPTIONS https://api.example.com/users
# OPTIONS with CORS preflight headers
curl -X OPTIONS \
-H "Origin: https://mysite.com" \
-H "Access-Control-Request-Method: POST" \
https://api.example.com/users
# check allowed methods from Allow header
curl -X OPTIONS -s -D - -o /dev/null https://api.example.com | grep -i allowHeaders
Setting Headers (-H)
-H (--header) adds a request header. Repeat -H for multiple headers. A trailing semicolon (X-Empty;) sends an empty value. @headers.txt reads headers from a file (one 'Name: value' per line) — handy for keeping Authorization tokens out of shell history.
# single header
curl -H "Accept: application/json" https://api.example.com/data
# multiple headers
curl -H "Accept: application/json" \
-H "Authorization: Bearer TOKEN" \
-H "X-Custom: value" \
https://api.example.com/data
# header with empty value
curl -H "X-Empty;" https://example.com
# headers from a file (one per line)
curl -H @headers.txt https://example.com
# Content-Type for POST
curl -X POST -H "Content-Type: application/json" \
-d '{"a":1}' https://api.example.comCommon Headers
The most common headers: Accept tells the server what format you want; Content-Type describes the request body; Authorization carries credentials. --compressed asks curl to request compressed responses (gzip, br) and auto-decompress them, so you don't need to handle encoding manually.
# Accept: negotiate response format
curl -H "Accept: application/json" https://api.example.com
curl -H "Accept: text/html" https://example.com
# Content-Type: describe request body
curl -H "Content-Type: application/json" -d '{"a":1}' https://api.example.com
curl -H "Content-Type: application/x-www-form-urlencoded" -d "a=1" https://api.example.com
# Authorization: credentials
curl -H "Authorization: Bearer TOKEN" https://api.example.com
# Accept-Encoding: ask for compression
curl -H "Accept-Encoding: gzip, br" --compressed https://example.com
# Cache-Control
curl -H "Cache-Control: no-cache" https://api.example.comUser-Agent
Some servers block the default curl User-Agent. -A (--user-agent) sets it; an empty -A removes it entirely. The long form is -H 'User-Agent: ...'. Many scraping-blocking sites check for real browser signatures, so a realistic browser UA is often required.
# default User-Agent looks like: curl/8.7.1
curl -v https://example.com 2>&1 | grep -i user-agent
# set a custom User-Agent
curl -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" https://example.com
curl -H "User-Agent: MyBot/1.0" https://example.com
# empty User-Agent
curl -A "" https://example.com
# fake a real browser (some sites require it)
curl -A "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) \
AppleWebKit/537.36 (KHTML, like Gecko) \
Chrome/120.0 Safari/537.36" https://example.com
# per-site default via config
# user-agent = "MyBot/1.0" in ~/.curlrcReferer & Host
-e (--referer) sets the Referer header. Overriding Host is essential when testing virtual hosts by IP: the server picks the right site based on Host. CORS-aware endpoints often require Origin; X-Forwarded-For/Proto simulate requests behind a proxy or load balancer.
# set Referer header
curl -H "Referer: https://google.com" https://example.com/page
curl -e https://google.com https://example.com/page
# set Host header (override DNS)
curl -H "Host: api.example.com" https://192.168.1.10/users
# combined: test a vhost by IP with custom Host
curl -H "Host: api.example.com" \
-H "X-Forwarded-Proto: https" \
http://10.0.0.5/health
# Origin header (for CORS/POST forms)
curl -H "Origin: https://mysite.com" \
-X POST -d "x=1" https://api.example.com/form
# X-Forwarded-For (debugging proxies)
curl -H "X-Forwarded-For: 1.2.3.4" https://api.example.comRemoving Headers
Providing -H 'Header-Name:' (with a colon but no value) tells curl to send no value for that header, effectively removing a default header like User-Agent or Accept-Encoding. curl replaces a header if you specify the same name again rather than sending duplicates. Useful for sending the bare minimum request.
# remove a header by giving it no value
curl -H "User-Agent:" https://example.com
# remove Accept-Encoding (disable compression)
curl -H "Accept-Encoding:" https://example.com
# remove all auto-added headers and send minimal request
curl -H "Host:" -H "User-Agent:" -H "Accept:" https://example.com
# prevent curl from adding Accept-Encoding
curl --no-keepalive https://example.com
# replace a default header instead of adding a duplicate
# (curl replaces, doesn't duplicate, when -H name matches)
curl -H "Accept: text/plain" https://example.comPOST Data & Forms
Form Data (-d)
-d (--data) sends URL-encoded form data and switches the method to POST. Multiple -d flags are joined with &. -d strips leading/trailing whitespace and newlines; --data-binary preserves them; --data-raw disables the @file feature entirely (so a literal value starting with @ is sent as-is).
# URL-encoded form data
curl -d "name=Alice&age=30" https://api.example.com/form
# explicit POST (same result; -d implies POST)
curl -X POST -d "name=Alice" https://api.example.com/form
# multiple -d are joined with &
curl -d "name=Alice" -d "age=30" https://api.example.com/form
# -d strips newlines; use --data-binary to preserve them
curl --data-binary "line1\nline2" https://api.example.com
# raw data, no content-type added
curl --data-raw "name=Alice&age=30" https://api.example.com
# default Content-Type: application/x-www-form-urlencoded
curl -v -d "a=1" https://api.example.com 2>&1 | grep -i content-typeURL-encoding
--data-urlencode percent-encodes the value (or the whole name=value), so you don't have to manually encode spaces, &, =, or non-ASCII. With name@file, the file's contents are URL-encoded as the value. Always use this for user-supplied data to avoid breaking the form structure.
# spaces and special chars must be encoded
curl -d "name=Alice%20Smith" https://api.example.com
curl -d "name=Alice+Smith" https://api.example.com
# --data-urlencode encodes the value for you
curl --data-urlencode "name=Alice Smith" https://api.example.com
curl --data-urlencode "[email protected]" https://api.example.com
# encode name=value pair (both encoded)
curl --data-urlencode "name=Alice & Bob" https://api.example.com
# encode only the value after =, with explicit content-type
curl --data-urlencode "query=SELECT * FROM users" \
-H "Content-Type: application/x-www-form-urlencoded" \
https://api.example.com/search
# encode content of a file
curl --data-urlencode [email protected] https://api.example.comBinary Data
--data-binary sends a file byte-for-byte (preserving newlines and null bytes), unlike -d which strips them. -T streams the file, so memory use stays low for large uploads. To send pre-compressed data, pipe it in with -H 'Content-Encoding: gzip' — the server must support this; otherwise use Transfer-Encoding: chunked.
# send a binary file as the body
curl --data-binary @image.png https://api.example.com/upload
# upload with explicit content type
curl --data-binary @image.png \
-H "Content-Type: image/png" \
https://api.example.com/upload
# raw bytes (no @ expansion, no stripping)
curl --data-raw "$(printf '\x00\x01\x02')" https://api.example.com
# stream a large file without buffering in memory
curl -T bigfile.iso https://api.example.com/upload
# send gzipped body
gzip -c data.json | curl --data-binary @- \
-H "Content-Encoding: gzip" \
-H "Content-Type: application/json" \
https://api.example.com/dataReading from File (@file)
-d @file reads the request body from a file (the @ triggers file reading). --data-binary @file preserves the bytes. -d @- reads from stdin — perfect for piping jq, curl, or other transformations. If a literal value starts with @ and you don't want file expansion, use --data-raw.
# read body from a file
curl -d @data.txt https://api.example.com/form
# read JSON body from a file
curl -H "Content-Type: application/json" \
-d @payload.json https://api.example.com
# binary-safe file read
curl --data-binary @image.png https://api.example.com/upload
# read from stdin
echo '{"a":1}' | curl -d @- https://api.example.com
# pipe through curl (transform then send)
jq '{name, age}' input.json | \
curl -H "Content-Type: application/json" -d @- https://api.example.com
# file name starting with @ (use --data-raw)
curl --data-raw "@mention" https://api.example.comContent-Type & Forms
Always match Content-Type to the body format: application/x-www-form-urlencoded (default for -d), application/json (with -H), multipart/form-data (auto-set by -F), text/xml for SOAP. Adding charset=utf-8 is good practice for non-ASCII data. Mismatches cause servers to reject or misparse the body.
# default for -d: application/x-www-form-urlencoded
curl -d "name=Alice" https://api.example.com
# JSON body
curl -H "Content-Type: application/json" \
-d '{"name":"Alice"}' https://api.example.com
# multipart form (file upload, see File Upload section)
curl -F "[email protected]" https://api.example.com/upload
# text/plain body
curl -H "Content-Type: text/plain" -d "hello" https://api.example.com
# XML body (SOAP)
curl -H "Content-Type: text/xml" \
-d '<?xml version="1.0"?><env:Envelope/>' \
https://api.example.com/soap
# set charset explicitly
curl -H "Content-Type: application/json; charset=utf-8" \
-d '{"name":"éàç"}' https://api.example.comJSON Requests
Basic JSON POST
JSON requests need -H 'Content-Type: application/json' plus -d with a JSON string. Use single quotes around the JSON so the shell doesn't touch double quotes inside. Pipe the response through jq '.' for readable output, or jq '.field' to extract a specific value.
# JSON POST request
curl -X POST \
-H "Content-Type: application/json" \
-d '{"name":"Alice","age":30}' \
https://api.example.com/users
# -d already implies POST, so -X is optional
curl -H "Content-Type: application/json" \
-d '{"name":"Alice"}' \
https://api.example.com/users
# ask for JSON back
curl -H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"name":"Alice"}' \
https://api.example.com/users
# pretty-print the JSON response (pipe to jq)
curl -s -H "Accept: application/json" \
https://api.example.com/users/1 | jq .JSON from File
For complex JSON, store it in a .json file and use -d @file (or --data-binary @file to preserve exact bytes including trailing newlines). Piping jq -n lets you build JSON programmatically without fiddly escaping. Always validate with jq . first — a syntax error wastes a request.
# POST JSON body from a file
curl -H "Content-Type: application/json" \
-d @payload.json https://api.example.com/users
# binary-safe (preserves exact bytes)
curl -H "Content-Type: application/json" \
--data-binary @payload.json https://api.example.com/users
# generate JSON with jq, then POST
jq -n '{name: "Alice", age: 30}' | \
curl -H "Content-Type: application/json" \
-d @- https://api.example.com/users
# validate JSON before sending
jq . payload.json && \
curl -H "Content-Type: application/json" \
-d @payload.json https://api.example.com/users
# send a JSON array
curl -H "Content-Type: application/json" \
-d '[{"id":1},{"id":2}]' https://api.example.com/batchJSON with Variables
Interpolating variables into JSON strings breaks on quotes and special chars. The safe pattern is jq --arg/--argjson: it builds valid JSON and escapes values correctly. --arg for strings, --argjson for numbers/booleans. For secrets, prefer a header file (-H @file) over command-line args to avoid leaking via ps.
# interpolate a shell variable into JSON (risky for special chars)
NAME="Alice"
curl -H "Content-Type: application/json" \
-d "{\"name\":\"$NAME\"}" https://api.example.com
# safer: build JSON with jq
NAME="Alice"
jq -n --arg name "$NAME" '{name: $name}' | \
curl -H "Content-Type: application/json" -d @- https://api.example.com
# multiple variables
NAME="Alice"; AGE=30
jq -n --arg name "$NAME" --argjson age "$AGE" \
'{name: $name, age: $age}' | \
curl -H "Content-Type: application/json" -d @- https://api.example.com
# read secret from env, hide from ps
TOKEN="$API_TOKEN" curl -H "Authorization: Bearer $API_TOKEN" \
-d '{"x":1}' https://api.example.comGraphQL Requests
GraphQL uses a single POST endpoint with a JSON body containing a 'query' string and optional 'variables'. Writing the query inline requires escaping inner double quotes, so store complex queries in a .json file and use -d @file. Introspection (the __schema query) lists the API's types and is great for exploration.
# GraphQL query as JSON body
curl -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer TOKEN" \
-d '{"query":"{ user(id:1) { name email } }"}' \
https://api.example.com/graphql
# query with variables
curl -X POST \
-H "Content-Type: application/json" \
-d '{"query":"query($id:Int!){ user(id:$id){name} }","variables":{"id":1}}' \
https://api.example.com/graphql
# query from a file (multi-line, no escaping pain)
curl -X POST \
-H "Content-Type: application/json" \
-d @query.json https://api.example.com/graphql
# introspection: list all types
curl -s -X POST -H "Content-Type: application/json" \
-d '{"query":"{ __schema { types { name } } }"}' \
https://api.example.com/graphql | jqContent Negotiation
Accept negotiates the response format; many REST APIs return JSON by default but support XML or CSV alternatives. --compressed requests gzip/brotli and decompresses automatically. APIs often version via Accept (e.g., GitHub: application/vnd.github+json). Check the actual Content-Type the server returns to confirm negotiation worked.
# request JSON
curl -H "Accept: application/json" https://api.example.com/users
# request XML
curl -H "Accept: application/xml" https://api.example.com/users
# request a specific version
curl -H "Accept: application/vnd.api+json; version=2" \
https://api.example.com/users
# request anything (default is */*)
curl -H "Accept: */*" https://example.com
# request compression and auto-decompress
curl --compressed -H "Accept-Encoding: gzip, br" https://example.com
# inspect what the server actually returns
curl -s -o /dev/null -D - https://api.example.com/users | grep -i content-typeFile Upload
Multipart Upload (-F)
-F (--form) builds a multipart/form-data request — the standard for file uploads. -F file=@path reads the file. Add ;filename= to override the name the server sees, and ;type= to set the part's Content-Type. Multiple files in the same field name (with [] in many frameworks) need multiple -F flags.
# upload a file as a multipart form field
curl -F "[email protected]" https://api.example.com/upload
# -F sets POST + multipart/form-data automatically
# the @ prefix reads the file
# add a text field alongside the file
curl -F "[email protected]" -F "description=My photo" \
https://api.example.com/upload
# rename the uploaded file (server sees "avatar.jpg")
curl -F "[email protected];filename=avatar.jpg" \
https://api.example.com/upload
# set explicit content-type for the file part
curl -F "[email protected];type=image/jpeg" \
https://api.example.com/upload
# upload multiple files in one field
curl -F "[email protected]" -F "[email protected]" \
https://api.example.com/uploadMultiple Files & Fields
Multipart forms can mix files and text fields. Use [] in field names for array-style uploads (frameworks like Rails/PHP parse these into arrays). A field can carry JSON by setting ;type=application/json — the server reads it as structured data rather than a file. Curl can't natively upload directories; tar them first.
# upload two files and a caption
curl -F "[email protected]" \
-F "[email protected]" \
-F "caption=Summer trip" \
https://api.example.com/gallery
# array-style field names (PHP/Rails style)
curl -F "files[][email protected]" -F "files[][email protected]" \
https://api.example.com/upload
# send a file's contents as a regular text field
curl -F "[email protected];type=application/json" \
https://api.example.com/upload
# mix file and JSON fields
curl -F "[email protected]" \
-F "options={\"priority\":\"high\"};type=application/json" \
https://api.example.com/upload
# upload a directory as a tar (workaround)
tar -cf - mydir/ | curl -F "tar=@-;filename=dir.tar" \
https://api.example.com/uploadStreaming Upload (-T)
-T (--upload-file) uses PUT and streams the file, so memory stays flat even for huge uploads — far better than -d @file for big data. -T - reads from stdin. -C - resumes an interrupted transfer. With a trailing slash on the URL, curl appends the local filename (like HTTP PUT mirroring).
# upload a file with PUT (streaming, low memory)
curl -T localfile.txt https://example.com/upload/remote.txt
# upload to a URL with the local filename
curl -T localfile.txt https://example.com/upload/
# stream from stdin (pipe into PUT body)
cat data.json | curl -T - https://api.example.com/data
# upload with authentication
curl -T file.zip -u user:pass \
https://example.com/upload/file.zip
# upload with progress bar
curl -T bigfile.iso --progress-bar \
https://example.com/upload/bigfile.iso
# resume an interrupted upload
curl -T bigfile.iso -C - https://example.com/upload/bigfile.isoUpload with Progress
By default curl shows a progress meter on stderr. --progress-bar switches to a simpler bar. -s (silent) hides it; add S (-sS) to still show errors. Progress info goes to stderr, so stdout (the body) stays clean — useful when piping. Use -w with %{size_upload} and %{time_total} for a one-line summary.
# default progress meter (terminal)
curl -T file.zip https://example.com/upload
# simple progress bar
curl -T file.zip --progress-bar https://example.com/upload
# no progress at all (silent)
curl -T file.zip -s https://example.com/upload
# show progress but only errors (silent + show-error)
curl -T file.zip -sS https://example.com/upload
# custom progress with --write-out
curl -T file.zip -w "Uploaded %{size_upload} bytes in %{time_total}s\n" \
-o /dev/null https://example.com/upload
# redirect progress to stderr, body to file
curl -T file.zip --progress-bar 2> progress.log \
-o response.txt https://example.com/uploadUpload Fields & Types
Curl guesses the MIME type from the file extension. Override with ;type=. The ;filename= option changes what the server sees, useful for sanitized names. The < prefix reads a file's contents into a text field (rather than sending it as a file) — helpful when the API expects a string but you have the data in a file.
# specify MIME type explicitly
curl -F "[email protected];type=text/csv" https://api.example.com/import
# let curl guess from the extension (default)
curl -F "[email protected]" https://api.example.com/upload
# force generic type
curl -F "[email protected];type=application/octet-stream" \
https://api.example.com/upload
# send a file with a custom filename visible to server
curl -F "upload=@/tmp/abc123;filename=report.pdf;type=application/pdf" \
https://api.example.com/upload
# send a string as if it were a file
curl -F "file=hello world" https://api.example.com/upload
# send a string as a file with a filename
curl -F "file=<inline.txt" https://api.example.com/uploadAuthentication
Basic Auth (-u)
-u (--user) sends HTTP Basic auth (base64-encoded user:pass). Omit the password to be prompted (safer — keeps it out of shell history). --netrc reads credentials from ~/.netrc so they never appear on the command line. Basic is insecure over plain HTTP; always use HTTPS. Anyone with shell access can see -u args via ps.
# HTTP Basic authentication
curl -u user:pass https://api.example.com/secret
curl --user user:pass https://api.example.com/secret
# prompt for password (don't put it on the command line)
curl -u user https://api.example.com/secret
# basic auth with explicit method (curl picks the safest by default)
curl -u user:pass --basic https://api.example.com/secret
# use a .netrc file instead of -u
echo "machine example.com login user password pass" > ~/.netrc
curl --netrc https://example.com/secret
# base64-encode credentials manually (equivalent)
curl -H "Authorization: Basic $(echo -n user:pass | base64)" \
https://api.example.com/secretBearer Token
Bearer tokens (OAuth2, JWT) go in the Authorization header. Avoid putting tokens on the command line — ps exposes them to other users. Read from a file (-H @file) or environment variable, and use a headers file for sensitive values. The refresh-token snippet shows the standard OAuth2 client-credentials flow.
# Bearer token (OAuth2 / JWT)
curl -H "Authorization: Bearer ACCESS_TOKEN" \
https://api.example.com/protected
# token from an environment variable
curl -H "Authorization: Bearer $TOKEN" \
https://api.example.com/protected
# token from a file (avoids shell history and ps)
curl -H "Authorization: Bearer $(cat ~/.token)" \
https://api.example.com/protected
# headers from a file (cleanest for secrets)
echo "Authorization: Bearer $TOKEN" > /tmp/hdr.txt
curl -H @/tmp/hdr.txt https://api.example.com/protected
rm /tmp/hdr.txt
# refresh token flow (simplified)
TOKEN=$(curl -s -u client:secret -d "grant_type=refresh_token" \
-d "refresh_token=$REFRESH" \
https://auth.example.com/token | jq -r .access_token)
curl -H "Authorization: Bearer $TOKEN" https://api.example.com/dataAPI Key in Header
API keys appear in custom headers (X-API-Key, X-Client-Id) or as query parameters. Query strings leak into server logs and browser history, so prefer headers. For AWS, --aws-sigv4 signs the request with your access/secret keys. GitHub uses Bearer with a personal access token (ghp_...).
# API key in a custom header (varies by service)
curl -H "X-API-Key: abc123" https://api.example.com/data
# API key as a query parameter
curl "https://api.example.com/data?api_key=abc123"
# GitHub personal access token
curl -H "Authorization: Bearer ghp_TOKEN" \
https://api.github.com/user
# AWS-style signature v4 (use the AWS CLI or aws-sigv4)
curl --aws-sigv4 "aws:amz:us-east-1:service" \
-u "ACCESS:SECRET" https://service.amazonaws.com/
# multiple keys
curl -H "X-API-Key: abc123" \
-H "X-Client-Id: my-app" \
https://api.example.com/dataNetrc Files
--netrc reads credentials from ~/.netrc, mapping hosts to login/password pairs. The file must be chmod 600 or curl refuses to use it. --netrc-file points to a custom location. The 'default' entry applies to any host without a specific machine entry. This is the cleanest way to keep secrets off the command line.
# ~/.netrc stores credentials per host
# format:
# machine api.example.com
# login alice
# password secret
# machine github.com
# login alice
# password ghp_TOKEN
# use netrc automatically
curl --netrc https://api.example.com/data
# use a custom netrc file
curl --netrc-file ~/.netrc.work https://api.example.com/data
# ignore netrc for this request
curl --netrc-optional -u user:pass https://api.example.com/data
# secure the file (required by curl on some systems)
chmod 600 ~/.netrc
# machine + default for any host
# default
# login anonymous
# password guestDigest & NTLM
Basic sends credentials in plaintext (over HTTPS that's fine). Digest sends a hash, never the password. NTLM and Negotiate handle Windows/Kerberos SSO. --anyauth lets curl pick based on the server's WWW-Authenticate header. The server must support the method; -v shows which challenges it issues.
# digest authentication (challenge-response, more secure than Basic)
curl -u user:pass --digest https://api.example.com/secret
# NTLM (Windows auth)
curl -u user:pass --ntlm https://intranet.example.com/
# negotiate (Kerberos/SPNEGO)
curl -u user:pass --negotiate https://intranet.example.com/
# anyauth: let curl pick the method
curl -u user:pass --anyauth https://api.example.com/secret
# specify the service principal for negotiate
curl -u user:pass --negotiate --service-name HTTP/intranet.example.com \
https://intranet.example.com/
# show which method the server requested
curl -v -u user:pass --anyauth https://api.example.com 2>&1 | grep -i www-authenticateOAuth & Tokens
OAuth2 has several grant types: client_credentials (server-to-server), authorization_code (after a browser login), password (legacy). The token endpoint returns an access_token (and often a refresh_token); use the access_token as a Bearer header for subsequent requests. App passwords (GitLab, GitHub) work with -u and bypass 2FA restrictions.
# OAuth2 client credentials grant
curl -u client_id:client_secret \
-d "grant_type=client_credentials" \
-d "scope=read write" \
https://auth.example.com/oauth/token
# OAuth2 authorization code (after browser redirect)
curl -d "grant_type=authorization_code" \
-d "code=AUTH_CODE" \
-d "redirect_uri=https://app.example.com/callback" \
-u client_id:client_secret \
https://auth.example.com/oauth/token
# OAuth2 password grant (deprecated but still seen)
curl -d "grant_type=password" \
-d "username=user" -d "password=pass" \
-u client_id:client_secret \
https://auth.example.com/oauth/token
# use the access token
curl -H "Authorization: Bearer $ACCESS_TOKEN" \
https://api.example.com/me
# basic auth with username and app password (e.g., GitLab)
curl -u "user:app_password" https://gitlab.example.com/api/v4/userProxy
HTTP/HTTPS Proxy
-x (--proxy) routes the request through a proxy. http_proxy/https_proxy/no_proxy environment variables are honored automatically. For HTTPS targets through an HTTP proxy, curl uses CONNECT to tunnel. Set no_proxy to bypass the proxy for internal hosts — comma-separated, with a leading dot meaning subdomains.
# use a proxy for all requests
curl -x http://proxy.example.com:8080 https://example.com
curl --proxy http://proxy.example.com:8080 https://example.com
# HTTPS proxy (proxy connection itself is TLS)
curl -x https://proxy.example.com:8443 https://example.com
# proxy via environment variable
export https_proxy=http://proxy.example.com:8080
curl https://example.com
# proxy for a specific scheme only
curl --proxy http://proxy:8080 http://example.com
curl --proxy https://secure-proxy:8443 https://example.com
# bypass proxy for specific hosts
export no_proxy="localhost,127.0.0.1,.internal.example.com"
curl https://internal.example.com # direct, no proxySOCKS Proxy
SOCKS5 is protocol-agnostic, so it tunnels HTTP, HTTPS, and more. --socks5-hostname has the proxy resolve the DNS (useful for reaching hosts your machine can't resolve). A common trick: ssh -D 1080 creates a local SOCKS proxy that tunnels traffic through a remote SSH server — great for secure browsing on untrusted networks.
# SOCKS5 proxy
curl -x socks5://proxy.example.com:1080 https://example.com
curl --socks5 proxy.example.com:1080 https://example.com
# SOCKS5 with local DNS resolution (server resolves)
curl --socks5-hostname proxy.example.com:1080 https://example.com
# SOCKS4 proxy
curl --socks4 proxy.example.com:1080 https://example.com
# SSH tunnel as a SOCKS proxy (run separately)
# ssh -D 1080 user@remote -N
curl -x socks5://localhost:1080 https://example.com
# SOCKS proxy with authentication
curl -x socks5://user:[email protected]:1080 https://example.comProxy Authentication
Proxies often require authentication. -U (--proxy-user) provides credentials; --proxy-ntlm/--proxy-digest/--proxy-anyauth pick the scheme. Embedding user:pass in the -x URL is equivalent. For NTLM (corporate Windows proxies), --proxy-ntlm handles the multi-step handshake. -U user (no password) prompts interactively.
# proxy with username/password
curl -x http://user:[email protected]:8080 https://example.com
# separate proxy auth flag
curl -x http://proxy.example.com:8080 -U user:pass https://example.com
curl --proxy-user user:pass -x http://proxy.example.com:8080 https://example.com
# prompt for proxy password
curl -U user -x http://proxy.example.com:8080 https://example.com
# NTLM-authenticated proxy
curl -U user:pass --proxy-ntlm \
-x http://proxy.example.com:8080 https://example.com
# digest-authenticated proxy
curl -U user:pass --proxy-digest \
-x http://proxy.example.com:8080 https://example.com
# anyauth for proxy
curl -U user:pass --proxy-anyauth \
-x http://proxy.example.com:8080 https://example.comNo Proxy & Bypass
--noproxy (or the no_proxy env var) lists hosts that bypass the proxy. A leading dot means 'this domain and all subdomains'. '*' bypasses the proxy for everything. CIDR ranges work in recent curl. This is essential when an external proxy can't reach internal corporate hosts.
# bypass proxy for specific hosts
curl --noproxy "localhost,127.0.0.1,.internal.com" \
-x http://proxy:8080 https://example.com
# via environment variable
export no_proxy="localhost,127.0.0.1,.internal.com,10.0.0.0/8"
curl https://internal.com # direct
# wildcard match for a domain and all subdomains
export no_proxy=".example.com"
curl https://api.example.com # bypasses proxy
# disable proxy entirely for one request
curl --noproxy "*" -x http://proxy:8080 https://example.com
# ignore *_proxy env vars for one request
curl --noproxy "*" https://example.com
# CIDR ranges (curl 7.86+)
export no_proxy="192.168.0.0/16,10.0.0.0/8"Proxy Headers & Tunneling
--proxy-header adds headers meant for the proxy itself (like Proxy-Authorization), not forwarded to the target. -p (--proxytunnel) forces curl to use CONNECT even for plain HTTP (normally CONNECT is only used for HTTPS). This is useful when the proxy intercepts plain HTTP and you want an end-to-end tunnel.
# add a header sent only to the proxy (not the target)
curl -x http://proxy:8080 \
--proxy-header "Proxy-Authorization: Bearer xyz" \
https://example.com
# custom header to proxy + normal header to target
curl -x http://proxy:8080 \
--proxy-header "X-Proxy-Tag: abc" \
-H "X-Target-Tag: def" \
https://example.com
# force CONNECT tunneling even for HTTP
curl -p -x http://proxy:8080 http://example.com
curl --proxytunnel -x http://proxy:8080 http://example.com
# inspect the CONNECT request to the proxy
curl -v -x http://proxy:8080 https://example.com 2>&1 | grep -i connect
# disable proxy tunneling (send HTTP directly through proxy)
curl --proxytunnel -p -x http://proxy:8080 http://example.comSSL/TLS
Skip Verification (--insecure)
-k (--insecure) skips certificate verification — use only for self-signed dev certs, never in production, as it allows MITM attacks. The default verifies both the certificate chain and the hostname. --resolve lets you pin an IP while keeping the correct Host/SNI — useful for testing a specific backend behind a load balancer.
# skip certificate verification (DANGEROUS)
curl -k https://self-signed.example.com
curl --insecure https://self-signed.example.com
# verify normally (default)
curl https://example.com
# show the cert chain
curl -v https://example.com 2>&1 | grep -E "SSL|cert"
# only verify the cert, not the hostname
curl --insecure https://example.com # both off
# (no built-in flag to verify cert but skip hostname;
# use a custom CA bundle instead)
# connect to a host with a mismatched certificate
curl -k --resolve example.com:443:1.2.3.4 https://example.com
# debug TLS handshake
curl -v https://example.com 2>&1 | grep -E "TLS|SSL|cert|cipher"Client Certificates
Mutual TLS (mTLS) requires both a client cert (--cert) and its private key (--key). PEM is default; --cert-type P12 handles PKCS#12 bundles. --pass provides the key password. --cacert overrides the trusted CA list (default: system store) — useful for internal CAs. mTLS is common in zero-trust and service-mesh setups.
# client certificate + private key (mTLS)
curl --cert client.pem --key client.key \
https://mtls.example.com/api
# PKCS#12 bundle (combined cert + key)
curl --cert client.p12 --cert-type P12 \
https://mtls.example.com/api
# password-protected key
curl --cert client.pem --key client.key \
--pass secret https://mtls.example.com/api
# PEM cert with password
curl --cert client.pem:secret https://mtls.example.com/api
# specify CA bundle to verify the server
curl --cacert custom-ca.pem https://internal.example.com
# mutual TLS with custom CA
curl --cert client.pem --key client.key \
--cacert internal-ca.pem \
https://mtls.internal.example.comTLS Version
--tlsv1.X sets the minimum version; --tls-max caps it. Forcing 1.2+ disables legacy protocols (SSLv3, TLS 1.0/1.1) that have known weaknesses. The default negotiates the highest mutually supported version. -v reveals the negotiated version and cipher in the 'SSL connection using' line.
# force a minimum TLS version
curl --tlsv1.2 https://example.com
curl --tlsv1.3 https://example.com
# force an exact TLS version
curl --tls-max 1.2 https://example.com # cap at 1.2
curl --tls-max 1.3 https://example.com
# negotiate the highest version both support (default)
curl https://example.com
# disable specific versions
curl --tlsv1.2 --tls-max 1.2 https://example.com # only 1.2
# list versions curl supports
curl --version | grep -i tls
# debug which version was negotiated
curl -v https://example.com 2>&1 | grep -i "SSL connection"Ciphers & CA Bundle
--ciphers restricts the cipher suite (OpenSSL syntax). --cacert points to a custom CA bundle for verifying servers with internal/private CAs — the system bundle (/etc/ssl/certs or macOS keychain) is used by default. If the CA bundle is outdated, servers with valid certs may be rejected; updating curl (or the ca-certificates package) fixes this.
# specify allowed ciphers
curl --ciphers "AES256-GCM-SHA384:AES128-GCM-SHA256" \
https://example.com
# use a custom CA bundle to verify the server
curl --cacert /etc/ssl/certs/internal-ca.pem \
https://internal.example.com
# use the system CA store (default)
curl https://example.com
# use Mozilla's CA bundle (download separately)
curl --cacert cacert.pem https://example.com
# show the cipher used
curl -v https://example.com 2>&1 | grep -i cipher
# disable certificate revocation checks (CRL/OCSP)
curl --cacert ca.pem --no-clobber https://example.comCertificate Inspection
Use -v to inspect the server's certificate (subject, issuer, validity dates). --pinnedpubkey enforces certificate pinning — the request fails if the server's public key doesn't match, defeating rogue CAs. For deep TLS debugging, --trace-ascii dumps the entire handshake. openssl s_client is a more powerful alternative for cert inspection.
# show the server certificate in verbose mode
curl -v https://example.com 2>&1 | grep -A1 "server certificate"
# extract certificate details (subject, issuer, dates)
curl -v https://example.com 2>&1 | grep -E "subject|issuer|start date|expire date"
# download the server certificate (PEM)
curl -v https://example.com 2>&1 | \
awk '/-----BEGIN CERT/,/-----END CERT/' > server.pem
# verify cert only, don't transfer body
curl -I https://example.com
# pin a specific certificate (fingerprint)
curl --pinnedpubkey sha256//BASE64= https://example.com
# check OCSP stapling
curl -v https://example.com 2>&1 | grep -i ocsp
# show full TLS trace
curl --trace-ascii - https://example.com | grep -i certDownloading
Save to File (-o/-O)
-o writes to a chosen filename; -O uses the remote filename (from the URL's basename). --output-dir sets the destination folder for -O. Multiple -O flags download multiple files in one invocation. -o /dev/null discards the body — useful for triggering webhooks or measuring timing without saving data.
# save with a custom name
curl -o page.html https://example.com/index.html
# save with the remote filename
curl -O https://example.com/file.zip
# save to a specific directory
curl -O --output-dir /tmp/downloads https://example.com/file.zip
# save with remote name to a specific dir
curl -o /tmp/file.zip https://example.com/file.zip
# download multiple files with remote names
curl -O https://example.com/a.zip -O https://example.com/b.zip
# save to /dev/null (just trigger the request)
curl -o /dev/null https://example.com/webhookResume Downloads (-C)
-C - (--continue-at -) resumes a download from where it left off, using the existing file size as the byte offset. The server must support Range requests (check Accept-Ranges: bytes). Combine with --retry to handle flaky connections — curl retries and resumes, avoiding wasted bandwidth.
# resume a partial download
curl -C - -o file.zip https://example.com/file.zip
curl --continue-at - -o file.zip https://example.com/file.zip
# resume with -O (remote name)
curl -C - -O https://example.com/file.zip
# start download at a specific byte offset
curl -C 1024 -o file.zip https://example.com/file.zip
# server must support Range requests
curl -I https://example.com/file.zip | grep -i accept-ranges
# resume an interrupted large download
curl -C - --progress-bar -O https://example.com/bigfile.iso
# combine with retry for unreliable connections
curl -C - --retry 5 --retry-all-errors -O \
https://example.com/bigfile.isoProgress Bar
--progress-bar shows a simpler ### bar instead of the default meter. -s silences all progress; -sS keeps errors. Progress goes to stderr, so it doesn't pollute the body on stdout — you can capture both separately. -w with %{size_download} and %{time_total} gives a clean one-line summary for scripts.
# default progress meter
curl -O https://example.com/file.zip
# simple progress bar (alternative style)
curl --progress-bar -O https://example.com/file.zip
# silent (no progress, no body output)
curl -s -O https://example.com/file.zip
# silent but still show errors
curl -sS -O https://example.com/file.zip
# show only errors (no progress, body to file)
curl -sS -o file.zip https://example.com/file.zip
# custom one-line progress
curl -o file.zip -w "%{size_download} bytes in %{time_total}s\n" \
https://example.com/file.zip
# redirect progress to a file, body to another
curl -o file.zip --progress-bar 2> progress.log \
https://example.com/file.zipDownload Range
-r (--range) requests a byte range, returning 206 Partial Content. Useful for previewing large files, resuming, or parallel multi-connection downloads. The server must advertise Accept-Ranges: bytes. Combine with shell backgrounding (&) to download ranges concurrently, then concatenate — the basis of download accelerators like aria2.
# download bytes 0-1023 (first 1KB)
curl -r 0-1023 -o part1.bin https://example.com/file.zip
# download from byte 1024 to end
curl -r 1024- -o rest.bin https://example.com/file.zip
# download last 500 bytes
curl -r -500 -o tail.bin https://example.com/file.zip
# check if server supports ranges
curl -I https://example.com/file.zip | grep -i accept-ranges
# Accept-Ranges: bytes -> supported
# parallel range download (split a large file)
curl -r 0-49999999 -o part1.bin https://example.com/big.iso &
curl -r 50000000-99999999 -o part2.bin https://example.com/big.iso &
wait
cat part1.bin part2.bin > big.iso
# HTTP/1.1 206 Partial Content confirms success
curl -r 0-100 -v https://example.com/file.zip 2>&1 | grep HTTPMultiple Files & Globbing
URL globbing ([1-3], {a,b,c}) issues one request per expansion. --output-dir sets where -O saves them. For true parallelism (curl itself is sequential), pipe URLs to xargs -P N to run N curls concurrently. This dramatically speeds up downloading many small files from the same host (respect the server's rate limits).
# download file1.zip, file2.zip, file3.zip
curl -O https://example.com/file[1-3].zip
# download with a step (1, 3, 5, 7, 9)
curl -O https://example.com/file[1-9:2].zip
# download from multiple paths
curl -O https://example.com/{a,b,c}.zip
# download to a specific directory
curl -O --output-dir /tmp/downloads \
https://example.com/file[1-3].zip
# sequential download (one at a time)
for url in https://example.com/{a,b,c}.zip; do
curl -O "$url"
done
# parallel download with xargs
echo "https://example.com/{a,b,c}.zip" | tr ' ' '\n' | \
xargs -n1 -P3 curl -OMirror & Recurse
Curl is single-URL by design — it doesn't recurse. To mirror a site, use wget --mirror or script link extraction with grep. For batch downloads, list URLs in a file and feed xargs -P N for parallelism. The sitemap example fetches and downloads every page indexed by a site — handy for offline snapshots.
# curl doesn't recurse like wget, but you can script it
# download a page and extract its links
curl -s https://example.com/ | \
grep -oP 'href="\K[^"]+' | grep '^http' > urls.txt
# download all linked PDFs
curl -s https://example.com/docs | \
grep -oP 'href="\K[^"]+\.pdf' | \
while read pdf; do curl -O "https://example.com/$pdf"; done
# use wget instead for true mirroring
# wget --mirror --convert-links --page-requisites https://example.com/
# download a list of URLs from a file
xargs -n1 -P4 curl -O < urls.txt
# fetch a sitemap and download every URL
curl -s https://example.com/sitemap.xml | \
grep -oP '<loc>\K[^<]+' | xargs -n1 -P4 curl -OTimeouts & Retries
Connection Timeout
--connect-timeout caps the time to establish the TCP/TLS connection (but not the transfer). Without it, curl may hang for minutes on unreachable hosts (OS default ~2 minutes). Always set both --connect-timeout (fail fast on dead hosts) and --max-time (cap the whole operation) in scripts to avoid hangs.
# max time to establish the connection (seconds)
curl --connect-timeout 10 https://example.com
# connect timeout in milliseconds (curl 7.32+)
curl --connect-timeout 2.5 https://example.com
# connect timeout + overall max time
curl --connect-timeout 10 --max-time 30 https://example.com
# DNS resolution timeout (curl 7.88+, otherwise OS-controlled)
curl --happy-eyeballs-timeout-ms 1000 https://example.com
# fail fast on unreachable hosts
curl --connect-timeout 3 -sS https://example.com || echo "unreachable"
# combine with --retry for resilience
curl --connect-timeout 5 --max-time 20 --retry 3 \
https://example.comMax Time
--max-time (-m) caps the entire operation (DNS + connect + transfer). --speed-time/--speed-limit abort if transfer drops below a byte rate for a duration — perfect for stalled downloads. Exit code 28 means timeout; check $? in scripts. Always set a max-time to prevent infinite hangs on slow or stuck servers.
# overall timeout for the whole operation
curl --max-time 30 https://example.com
curl -m 30 https://example.com
# max time in milliseconds (decimal)
curl --max-time 2.5 https://example.com
# max time with no data transfer (idle timeout)
curl --speed-time 30 --speed-limit 1000 https://example.com
# abort if speed < 1000 bytes/sec for 30 sec
# max time for a file upload
curl -T big.iso --max-time 600 https://example.com/upload
# max time per redirect hop
curl -L --max-time 60 https://example.com/redirect
# check exit code (28 = timed out)
curl --max-time 5 https://slow.example.com || echo "exit code: $?"Retries
--retry retries on transient errors (timeouts, 5xx, 429) by default; --retry-all-errors extends to all failures. --retry-delay adds a fixed wait (doubled each attempt) between retries. --retry-connrefused retries even when the connection is refused (e.g., server restarting). Pair with -C - to resume partial downloads. This is essential for resilient scripts.
# retry up to 3 times on transient errors
curl --retry 3 https://example.com
# retry on any error (not just transient)
curl --retry 3 --retry-all-errors https://example.com
# retry with exponential backoff
curl --retry 5 --retry-delay 2 https://example.com
# waits 2s, 4s, 6s, 8s, 10s between retries (curl 7.66+)
# retry on connection-refused specifically
curl --retry 3 --retry-connrefused https://example.com
# retry only on HTTP 5xx and 429
curl --retry 3 --retry-all-errors \
-H "Accept: application/json" https://api.example.com
# combine with resume for large downloads
curl --retry 5 --retry-all-errors -C - -O \
https://example.com/bigfile.isoRetry Delay & Backoff
--retry-delay N waits N seconds before the first retry, then doubles (N, 2N, 4N...). Curl honors the server's Retry-After header for 429/503 responses. For custom logic (jitter, max total wait, condition on status code), wrap curl in a shell loop. Always pair --retry with --max-time so retries can't run forever.
# fixed delay between retries
curl --retry 5 --retry-delay 2 https://example.com
# exponential backoff (curl doubles the delay each time)
curl --retry 5 --retry-delay 1 https://example.com
# waits: 1s, 2s, 4s, 8s, 16s
# honor Retry-After header (default with --retry)
curl --retry 3 https://example.com
# cap retry delay with a max-time
curl --retry 10 --retry-delay 1 --max-time 60 https://example.com
# custom backoff in a shell loop
for i in 1 2 3 4 5; do
curl -s --max-time 10 https://example.com && break
echo "attempt $i failed, waiting $((i*2))s..."
sleep $((i*2))
done
# retry on specific HTTP status codes
curl --retry 5 --retry-all-errors \
-w "%{http_code}" -o /dev/null -s https://example.comSpeed Limits
--speed-time/--speed-limit aborts a stalled transfer (no data for N seconds at a rate below the limit) — exit code 28. --limit-rate caps the transfer rate (supports K/M/G suffixes), useful for not saturating bandwidth or for testing slow networks. Combine with --retry to resume stalled downloads automatically.
# abort if speed drops below 1000 bytes/sec for 30 sec
curl --speed-limit 1000 --speed-time 30 -O \
https://example.com/bigfile.iso
# shorthand: --speed-time defaults to 30 if only --speed-limit is set
curl --speed-limit 1000 -O https://example.com/bigfile.iso
# limit max download rate (bytes/sec)
curl --limit-rate 100k -O https://example.com/file.zip
# limit rate with units (K, M, G)
curl --limit-rate 1M -O https://example.com/file.zip
# limit upload rate
curl -T big.iso --limit-rate 500k https://example.com/upload
# combine with retry on stall
curl --speed-limit 1000 --speed-time 30 \
--retry 5 --retry-all-errors \
-O https://example.com/bigfile.isoDebugging
Verbose (-v)
-v (verbose) is the primary debugging tool. '>' lines are the request, '<' lines are the response, '*' lines are curl internals (DNS, TLS, connection) — all on stderr so stdout stays clean for the body. Redirect 2>&1 to filter with grep. Use -o /dev/null to inspect headers without saving the body.
# verbose: full request + response + curl internals
curl -v https://example.com
# output goes to stderr; body to stdout
curl -v https://example.com > body.txt 2> debug.log
# only show request headers (> lines)
curl -v https://example.com 2>&1 | grep '^>'
# only show response headers (< lines)
curl -v https://example.com 2>&1 | grep '^<'
# only show curl info (* lines)
curl -v https://example.com 2>&1 | grep '^\*'
# verbose without sending the body
curl -v -o /dev/null https://example.comTrace
--trace dumps everything (including the body) in hex + ASCII; --trace-ascii skips the hex. --trace-time adds timestamps. Unlike -v, trace shows the request and response bodies, so it's the go-to for debugging binary or compressed data. Warning: --trace logs secrets (passwords, tokens) — scrub before sharing.
# trace: dump all sent and received data (hex + ASCII)
curl --trace - https://example.com
# trace ASCII only (no hex dump)
curl --trace-ascii - https://example.com
# trace to a file
curl --trace trace.log https://example.com
# trace including time per packet
curl --trace-ascii - --trace-time https://example.com
# trace with request body (hidden by default in -v)
curl --trace-ascii - -d "secret=data" https://example.com
# compare verbose vs trace
curl -v https://example.com 2>&1 | head
curl --trace-ascii - https://example.com | headHeaders Only
-I sends a HEAD request (most servers return the same headers as GET without the body). -D dumps headers to a file or '-' (stdout). To see what curl sent (request headers), use -v and grep '^>'. For just the status code, -w '%{http_code}' is the cleanest. Note: some servers respond differently to HEAD vs GET.
# HEAD request: response headers only
curl -I https://example.com
curl --head https://example.com
# dump response headers to stdout, discard body
curl -D - -o /dev/null https://example.com
# dump response headers to a file
curl -D headers.txt -o body.html https://example.com
# show request headers (what curl sent)
curl -v -o /dev/null https://example.com 2>&1 | grep '^>'
# show both request and response headers
curl -v -o /dev/null https://example.com 2>&1 | grep -E '^[<>]'
# show only the status line
curl -s -o /dev/null -w "%{http_code} %{http_version}\n" \
https://example.com
curl -D - -o /dev/null -s https://example.com | head -1Request & Response Separation
Verbose output goes to stderr, the body to stdout, so they can be captured separately with redirection. -D saves response headers; -o saves the body; -v (stderr) captures the full conversation. This separation is essential for debugging APIs: inspect headers and body independently, log timing, and compare request vs response.
# capture request and response separately
curl -v https://example.com \
-o body.txt \
1> /dev/null 2> verbose.log
# extract request only
curl -v https://example.com 2>&1 1>/dev/null | grep '^>' > request.txt
# extract response only
curl -v https://example.com 2>&1 1>/dev/null | grep '^<' > response.txt
# split headers and body cleanly
curl -s -D headers.txt -o body.txt https://example.com
# capture everything: body, headers, request, timing
curl -s -D headers.txt -o body.txt \
-w "@curl-format.txt" https://example.com >> timing.log
# show body to terminal, headers to file
curl -D headers.txt https://example.comDebug Options
--dry-run prints what curl would do without sending — great for verifying URL globbing and config. --resolve pins a host to an IP, bypassing DNS (essential for testing a specific backend). --interface binds to a network card or IP. --no-keepalive forces a fresh connection per request, useful for reproducing connection-level bugs.
# show what curl would do without sending
curl --dry-run https://example.com
# dump parsed config files
curl --config /dev/null -v https://example.com 2>&1 | grep -i config
# force a fresh connection (no keep-alive reuse)
curl --no-keepalive https://example.com
# disable DNS cache
curl --doh-url https://dns.example.com/dns-query https://example.com
# use a specific network interface
curl --interface eth0 https://example.com
curl --interface 192.168.1.5 https://example.com
# resolve a hostname to a specific IP (no DNS)
curl --resolve example.com:443:1.2.3.4 https://example.com
# debug globbing (list URLs without fetching)
curl --dry-run -O https://example.com/file[1-3].zipResponse Info
HTTP Status Code
-w '%{http_code}' extracts just the status code for scripting. -f (--fail) makes curl exit non-zero on 4xx/5xx (without it, curl returns 0 even for 404). --fail-with-body also prints the response body on error (more useful for debugging). Use the status code to branch in scripts: 2xx success, 3xx redirect, 4xx client error, 5xx server error.
# get only the status code
curl -s -o /dev/null -w "%{http_code}" https://example.com
echo # 200
# status code with newline
curl -s -o /dev/null -w "%{http_code}\n" https://example.com
# full status line
curl -s -o /dev/null -w "%{http_version} %{http_code}\n" https://example.com
# branch on status code
CODE=$(curl -s -o /dev/null -w "%{http_code}" https://example.com)
if [ "$CODE" = "200" ]; then echo "ok"; else echo "fail: $CODE"; fi
# fail on HTTP errors (4xx, 5xx)
curl -f https://example.com || echo "HTTP error"
curl --fail-with-body https://example.com # 7.76+: still print body
# check redirect chain status codes
curl -L -w "%{num_redirects} redirects, final %{http_code}\n" \
-o /dev/null -s https://short.url/abcWrite-out Format
-w (--write-out) formats info about the transfer using %{variable} placeholders. Timings (time_namelookup, time_connect, time_appconnect, time_total) break down where time is spent — essential for performance analysis. %{json} outputs all variables as JSON (curl 7.70+). Store a format in a file and use -w @file for reuse.
# built-in variables with -w / --write-out
curl -s -o /dev/null -w "HTTP %{http_code} in %{time_total}s\n" \
https://example.com
# URL info
curl -s -o /dev/null -w "URL: %{url_effective}\nIP: %{remote_ip}\n" \
https://example.com
# timing breakdown
curl -s -o /dev/null \
-w "dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} total=%{time_total}\n" \
https://example.com
# size info
curl -s -o /dev/null \
-w "downloaded=%{size_download} uploaded=%{size_upload}\n" \
https://example.com
# write format from a file
cat > fmt.txt <<EOF
http_code: %{http_code}
time_total: %{time_total}s
EOF
curl -s -o /dev/null -w "@fmt.txt" https://example.com
# JSON output (curl 7.70+)
curl -s -o /dev/null -w "%{json}" https://example.comResponse Time
The timing variables decompose the request: time_namelookup (DNS), time_connect (TCP), time_appconnect (TLS), time_starttransfer (TTFB — first byte received), time_total (entire transfer). TTFB is the key metric for API latency. Compare timings across endpoints to identify bottlenecks (DNS, network, TLS, or server processing).
# total time
curl -s -o /dev/null -w "%{time_total}\n" https://example.com
# DNS resolution time
curl -s -o /dev/null -w "%{time_namelookup}\n" https://example.com
# TCP connect time
curl -s -o /dev/null -w "%{time_connect}\n" https://example.com
# TLS handshake time (after connect)
curl -s -o /dev/null -w "%{time_appconnect}\n" https://example.com
# time to first byte (TTFB)
curl -s -o /dev/null -w "%{time_starttransfer}\n" https://example.com
# full timing breakdown
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
# compare two endpoints
curl -s -o /dev/null -w "%{time_total}\n" https://api1.example.com
curl -s -o /dev/null -w "%{time_total}\n" https://api2.example.comExtract Headers
-D - dumps response headers to stdout; pipe through grep to extract a specific header. -I (HEAD) works too but may differ from GET. -w provides %{url_effective} (final URL after redirects), %{remote_ip} (server IP), and %{num_redirects}. These are invaluable for debugging CDN routing, redirects, and server identification.
# dump all response headers to stdout, discard body
curl -s -D - -o /dev/null https://example.com
# extract one header (Content-Type)
curl -s -D - -o /dev/null https://example.com | \
grep -i "^content-type:"
# extract Content-Length (size)
curl -s -I https://example.com/file.zip | grep -i content-length
# extract Set-Cookie
curl -s -D - -o /dev/null https://example.com | grep -i set-cookie
# extract the final URL after redirects
curl -s -L -o /dev/null -w "%{url_effective}\n" https://short.url/abc
# extract the server's IP address
curl -s -o /dev/null -w "%{remote_ip}\n" https://example.com
# count redirects
curl -s -L -o /dev/null -w "%{num_redirects}\n" https://short.url/abcFormat Variables
-w (--write-out) supports dozens of variables (run 'curl -w "%{json}"' to see them all as JSON). The timing variables are the foundation of performance monitoring; the size variables track bandwidth; url_effective and num_redirects debug routing. %{json} (curl 7.70+) is the easiest way to capture everything for logging or dashboards.
# complete list of common -w variables
# %{http_code} - response status code
# %{http_version} - HTTP version (1.1, 2, 3)
# %{url_effective} - final URL after redirects
# %{remote_ip} - server IP address
# %{remote_port} - server port
# %{time_namelookup} - DNS resolution time
# %{time_connect} - TCP connect time
# %{time_appconnect} - TLS handshake time
# %{time_pretransfer} - time before transfer starts
# %{time_starttransfer} - TTFB (first byte)
# %{time_total} - total time
# %{size_download} - bytes downloaded
# %{size_upload} - bytes uploaded
# %{size_header} - header bytes
# %{size_request} - request bytes
# %{num_redirects} - number of redirects
# %{ssl_verify_result}- SSL verification result
# %{content_type} - response content type
# %{json} - all variables as JSON (7.70+)
# reference: curl -w "%{json}" https://example.com
curl -s -o /dev/null -w "%{json}" https://example.com | jqFTP & SMTP
FTP Basics
curl supports FTP for both downloads (GET) and uploads (PUT via -T). -u provides credentials; anonymous FTP uses 'anonymous' + an email. FTPS (--ftp-ssl) encrypts the control channel. Passive mode (default) works through NAT/firewalls; use --ftp-port for active mode if the server requires it.
# anonymous FTP download
curl ftp://ftp.example.com/file.txt
# authenticated FTP
curl -u user:pass ftp://ftp.example.com/file.txt
# list a directory
curl ftp://ftp.example.com/directory/
# download a file
curl -O ftp://ftp.example.com/file.txt
# upload a file (PUT)
curl -T local.txt ftp://ftp.example.com/remote.txt -u user:pass
# use explicit FTPS (FTP over TLS)
curl --ftp-ssl ftp://ftp.example.com/file.txt -u user:pass
# passive mode (default; needed behind NAT/firewalls)
curl --ftp-pasv ftp://ftp.example.com/ -u user:passFTP Upload
-T uploads a file via FTP's STOR command. --ftp-create-dirs makes parent directories with MKD. --append uses APPE to add to an existing file. -C - resumes an interrupted upload. Globbing ({a,b}) uploads multiple files in one invocation. For directories, tar them first — FTP can't upload folders directly.
# upload a single file
curl -T local.txt ftp://ftp.example.com/remote.txt -u user:pass
# upload with explicit filename
curl -T local.txt -o remote.txt ftp://ftp.example.com/ -u user:pass
# upload to a directory
curl -T local.txt ftp://ftp.example.com/uploads/ -u user:pass
# create a directory (MKD command)
curl --ftp-create-dirs -T local.txt \
ftp://ftp.example.com/newdir/file.txt -u user:pass
# append to a remote file
curl -T local.txt --append ftp://ftp.example.com/remote.txt -u user:pass
# upload multiple files with globbing
curl -T "{file1,file2}.txt" ftp://ftp.example.com/ -u user:pass
# resume an interrupted upload
curl -C - -T bigfile.iso ftp://ftp.example.com/bigfile.iso -u user:passFTP Listing
An FTP URL ending in / lists the directory. --list-only returns just filenames. -v shows the underlying FTP commands (USER, PASS, PASV, LIST, RETR) — invaluable for debugging permission or firewall issues. Curl can't recurse into subdirectories; use wget --mirror or a script for recursive FTP downloads.
# list a directory (default format)
curl ftp://ftp.example.com/ -u user:pass
# list with --list-only (names only, no details)
curl --list-only ftp://ftp.example.com/ -u user:pass
# list a specific directory
curl ftp://ftp.example.com/pub/ -u user:pass
# recursive listing (curl doesn't recurse natively)
# use --ftp-method to control PASV/EPSV behavior
curl --ftp-method nocwd ftp://ftp.example.com/ -u user:pass
# download all files in a directory (with globbing)
curl -O ftp://ftp.example.com/pub/*.txt -u user:pass
# show server responses (FTP commands)
curl -v ftp://ftp.example.com/ -u user:pass 2>&1 | grep -E "^\*|^[<>]"SMTP Send Email
curl can send email via SMTP. The body (with headers) is in a file piped via -T. --mail-from and --mail-rcpt set the envelope (separate from the To/From headers in the body). --ssl-reqd forces STARTTLS (port 587); smtps:// uses implicit TLS (port 465). Auth via -u (LOGIN/PLAIN). Great for scripted alerts from servers without a mail client.
# send email via SMTP
curl --url "smtp://smtp.example.com:587" \
--mail-from "[email protected]" \
--mail-rcpt "[email protected]" \
-u "[email protected]:password" \
-T email.txt
# email body from a file
cat > email.txt <<EOF
From: [email protected]
To: [email protected]
Subject: Test from curl
Hello from curl!
EOF
curl --url "smtp://smtp.example.com:587" \
--mail-from "[email protected]" \
--mail-rcpt "[email protected]" \
-u "[email protected]:password" \
--ssl-reqd \
-T email.txt
# multiple recipients
curl --url "smtp://smtp.example.com:587" \
--mail-from "[email protected]" \
--mail-rcpt "[email protected]" \
--mail-rcpt "[email protected]" \
-T email.txt
# STARTTLS (port 587)
curl --ssl-reqd --url "smtp://smtp.example.com:587" \
--mail-from "[email protected]" --mail-rcpt "[email protected]" \
-T email.txt
# SMTPS (implicit TLS, port 465)
curl --url "smtps://smtp.example.com:465" \
--mail-from "[email protected]" --mail-rcpt "[email protected]" \
-T email.txtIMAP & POP3
curl can read mail via IMAP and POP3. The URL syntax includes folder, UID, and section selectors. IMAP supports search (?UNSEEN, ?SUBJECT etc.). imaps:// and pop3s:// use implicit TLS. Note: curl is read-only for mail — it can fetch but not send via IMAP/POP3 (use SMTP for sending). The body is returned as raw RFC 822 message text.
# list IMAP mailboxes
curl -u "user:pass" --url "imap://imap.example.com:143"
# list messages in INBOX
curl -u "user:pass" --url "imap://imap.example.com/INBOX"
# fetch a specific message (UID 1)
curl -u "user:pass" --url "imap://imap.example.com/INBOX;UID=1"
# fetch message headers only
curl -u "user:pass" --url "imap://imap.example.com/INBOX;UID=1;SECTION=HEADER"
# search for unread messages
curl -u "user:pass" --url "imap://imap.example.com/INBOX?UNSEEN"
# POP3: list messages
curl -u "user:pass" --url "pop3://pop.example.com/"
# POP3: retrieve message #1
curl -u "user:pass" --url "pop3://pop.example.com/1"
# use IMAPS (implicit TLS, port 993)
curl -u "user:pass" --url "imaps://imap.example.com/INBOX"FTPS & SFTP
FTP has three encrypted variants: FTPS explicit (--ftp-ssl, upgrades on port 21), FTPS implicit (ftps://, TLS from start on port 990), and SFTP (sftp://, completely different protocol over SSH on port 22). SFTP supports key auth via --key (with --pass for encrypted keys). SFTP is generally preferred over FTPS today for its simplicity and firewall-friendliness.
# FTPS: explicit TLS (upgrade to TLS on plain FTP port)
curl --ftp-ssl --ssl-reqd ftp://ftp.example.com/ -u user:pass
# FTPS: implicit TLS (TLS from the start, port 990)
curl ftps://ftp.example.com:990/ -u user:pass
# SFTP (SSH File Transfer Protocol, port 22)
curl -u user sftp://sftp.example.com/file.txt
# SFTP with key authentication
curl -u user --key ~/.ssh/id_rsa \
sftp://sftp.example.com/file.txt
# SFTP with a password-protected key
curl -u user --key ~/.ssh/id_rsa --pass passphrase \
sftp://sftp.example.com/file.txt
# SFTP upload
curl -u user -T local.txt sftp://sftp.example.com/remote.txt
# SFTP list directory
curl -u user sftp://sftp.example.com/Parallel & Batch
Parallel Downloads
Curl itself is sequential — to download concurrently, run multiple curls in parallel via & and wait, or use xargs -P N to run N at once. This dramatically speeds up many small files. Be considerate: too many parallel connections can overwhelm the server or get you rate-limited. 4-8 concurrent is a reasonable default.
# background multiple curls (shell parallelism)
curl -O https://example.com/a.zip &
curl -O https://example.com/b.zip &
curl -O https://example.com/c.zip &
wait
# parallel with xargs (4 concurrent)
echo -e "https://example.com/a.zip\nhttps://example.com/b.zip" | \
xargs -n1 -P4 curl -O
# parallel with a URL list file
xargs -n1 -P4 curl -O < urls.txt
# zsh-specific parallel with {..}
for url in https://example.com/{a,b,c}.zip; do
curl -O "$url" &
done; wait
# bash-specific using arrays and &
urls=("https://example.com/a.zip" "https://example.com/b.zip")
for url in "${urls[@]}"; do curl -O "$url" & done; waitxargs Parallel
xargs -P N runs N processes concurrently, feeding each line of stdin as an argument. -a reads from a file. -I {} lets you place the URL anywhere in a command. This is the simplest way to parallelize curl downloads without extra tools. For host-aware throttling or complex retry logic, use a dedicated download manager like aria2c.
# download each URL from a file, 4 concurrent
xargs -n1 -P4 -a urls.txt curl -O
# download with custom names, parallel
cat urls.txt | xargs -n2 -P4 sh -c 'curl -o "$0" "$1"'
# download and process each response
cat urls.txt | xargs -n1 -P4 -I {} sh -c \
'curl -s {} | jq .field'
# retry failures in parallel
xargs -n1 -P4 -a urls.txt curl --retry 3 -O
# limit total concurrent connections to a host
# (xargs doesn't know about hosts; use a queue or a download manager)
xargs -n1 -P4 -a urls.txt curl -O --limit-rate 1M
# show progress for parallel downloads
xargs -n1 -P4 -a urls.txt curl -O --progress-bar 2>&1URL Globbing
Globbing expands URLs into multiple requests: [1-10] ranges (with optional :step and zero-padding via leading zeros) and {a,b,c} lists. -o 'file#1.zip' uses #1 as the counter for custom output names. -g (--globoff) disables expansion — essential for URLs containing literal [ ] or { } (JSON, IPv6).
# numeric range: 1, 2, 3
curl -O https://example.com/file[1-3].zip
# range with step: 0, 5, 10, 15, 20
curl -O https://example.com/file[0-20:5].zip
# leading zeros (zero-padded)
curl -O https://example.com/file[01-10].zip # 01, 02, ..., 10
# list expansion: a, b, c
curl -O https://example.com/{a,b,c}.zip
# nested globs
curl -O https://example.com/{v1,v2}/file[1-2].txt
# save each with a custom name pattern
curl -o "file#1.zip" https://example.com/file[1-3].zip
# disable globbing (literal brackets)
curl -g "https://example.com/file[1].zip"Sequential Range
For sequential downloads (when parallelism would overwhelm the server or violate rate limits), loop over URLs in bash. Add sleep for politeness. -sf (silent + fail) makes curl exit non-zero on HTTP errors so the loop can break early. This is the safe pattern for scraping or crawling: be respectful, add delays, and handle errors.
# fetch URLs one at a time (no parallelism)
for i in 1 2 3 4 5; do
curl -O "https://example.com/page$i.html"
done
# range with brace expansion (bash/zsh)
for i in {1..10}; do
curl -s -O "https://example.com/page$i.html"
done
# with a delay between requests (rate limiting)
for i in {1..10}; do
curl -s -O "https://example.com/page$i.html"
sleep 1
done
# retry each URL individually
for url in $(cat urls.txt); do
curl -s --retry 3 -O "$url"
done
# conditional: stop on first error
for url in $(cat urls.txt); do
curl -sf -O "$url" || { echo "failed: $url"; break; }
doneGNU Parallel & curl
GNU parallel is more powerful than xargs: it preserves output order, shows progress (--bar), and handles arguments more flexibly. -j N sets concurrency. :::: reads from a file, ::: from the command line. Parallel is ideal for batch API testing, scraping, and benchmarks where you need clean, ordered output from concurrent requests.
# GNU parallel: smarter than xargs (preserves order, progress)
parallel -j4 curl -O {} ::: https://example.com/a.zip \
https://example.com/b.zip
# parallel from a file
parallel -j4 curl -O :::: urls.txt
# parallel with progress bar
parallel --bar -j4 curl -O :::: urls.txt
# parallel with output capture
parallel -j4 'curl -s -o {}.out {}' :::: urls.txt
# parallel with retry and timing
parallel -j4 'curl -s -w "{} %{time_total}s\n" -o /dev/null {}' :::: urls.txt
# parallel POST requests
parallel -j4 'curl -s -X POST -d "id={}" https://api.example.com/{}' \
::: 1 2 3 4 5
# combine with --dry-run to preview
parallel --dry-run -j4 curl -O :::: urls.txtConfig Files
.curlrc Basics
~/.curlrc holds default options applied to every curl invocation. Use long option names without the leading --. One option per line; values can be quoted. This keeps common settings (timeouts, retries, User-Agent) consistent across scripts and interactive use. Override per-request by passing the option explicitly on the command line.
# ~/.curlrc is read automatically by curl
# one option per line, long option names without the --
# ~/.curlrc example:
# show progress by default
silent
# always follow redirects
location
# default User-Agent
user-agent = "MyApp/1.0"
# default headers
header = "Accept: application/json"
# retry transient errors
retry = 3
# connect timeout
connect-timeout = 10
# max time
max-time = 30
# lines starting with # are comments
# blank lines are ignored
# test: curl https://example.com now uses all the aboveConfig File Location
Curl reads ~/.curlrc (or $CURL_HOME/.curlrc) by default. -K (--config) points to a specific file; -K - reads from stdin. -q (--disable) skips the config file entirely — useful when a default option breaks a specific request. CURL_HOME relocates the search directory for portable setups.
# default locations (searched in order):
# 1. ~/.curlrc (or $CURL_HOME/.curlrc)
# 2. /etc/curl/curlrc (system-wide, if compiled in)
# set a custom config directory
export CURL_HOME=/etc/curl
# curl then reads $CURL_HOME/.curlrc
# specify a config file explicitly
curl -K /path/to/myconfig https://example.com
curl --config /path/to/myconfig https://example.com
# read config from stdin
echo "silent" | curl -K - https://example.com
# disable config file reading
curl -q https://example.com
curl --disable https://example.com
# check which config curl reads
curl -v https://example.com 2>&1 | grep -i configConfig with -K
-K loads options from a file, perfect for per-environment configs (dev vs prod tokens, timeouts, headers). Storing secrets in a config file (chmod 600) keeps them out of shell history and ps. Multiple -K files are processed in order, so later files override earlier ones — useful for a base config plus environment-specific overrides.
# use a config file for a specific request
curl -K prod.conf https://api.example.com/data
# prod.conf:
# header = "Authorization: Bearer prod-token"
# header = "X-Environment: production"
# connect-timeout = 5
# max-time = 20
# dev.conf (different settings)
# header = "Authorization: Bearer dev-token"
# header = "X-Environment: development"
# max-time = 60
# switch environments by config file
curl -K dev.conf https://api.example.com/data
curl -K prod.conf https://api.example.com/data
# config file with URL
# echo "url = https://api.example.com/data" > req.conf
curl -K req.conf
# multiple config files (later ones override earlier)
curl -K defaults.conf -K override.conf https://example.comCommon Defaults
A solid ~/.curlrc makes every curl invocation safer and more consistent: timeouts prevent hangs, retries handle transient failures, --compressed handles gzip transparently, and a stripped User-Agent reduces fingerprinting. The write-out line logs timing and status for every request — invaluable for quick performance checks.
# ~/.curlrc with sensible production defaults
# always follow redirects
location
# limit redirects
max-redirs = 5
# timeouts (fail fast, cap total)
connect-timeout = 10
max-time = 60
# retry transient failures
retry = 3
retry-delay = 2
retry-all-errors
# show errors even when silent
show-error
# auto-decompress responses
compressed
# negotiate HTTP/2
http2
# verify SSL by default (explicit for clarity)
cacert = /etc/ssl/certs/ca-certificates.crt
# don't leak the curl version
user-agent = ""
# verbose timing on failure
write-out = "time: %{time_total}s code: %{http_code}\n"Multiple Configs
Splitting config into layered files (base, auth, api, environment) keeps things modular and secure. Process them with multiple -K flags in order — later files override earlier ones. Put secrets in a 600-permission file outside version control. A wrapper script that loads the stack lets you call 'mycurl' with all defaults applied.
# base.conf — shared defaults
# silent
# show-error
# compressed
# connect-timeout = 10
# max-time = 30
# auth.conf — credentials (chmod 600)
# user = alice:secret
# header = "Authorization: Bearer TOKEN"
# api.conf — API-specific options
# header = "Accept: application/json"
# header = "X-Client: my-app/2.0"
# retry = 5
# combine configs in order (later overrides earlier)
curl -K base.conf -K auth.conf -K api.conf \
https://api.example.com/data
# config file referencing another (nesting not supported)
# but you can source them in a wrapper script:
# #!/bin/bash
# curl -K base.conf -K auth.conf -K api.conf "$@"
# environment-specific overlay
curl -K base.conf -K "${ENV}.conf" https://api.example.comAdvanced Techniques
Connection Reuse
Within a single curl invocation, the TCP connection is reused across multiple URLs to the same host (HTTP keep-alive), saving the TLS handshake overhead. --no-keepalive disables this. --max-connects caps concurrent connections for multi-URL fetches. Across separate curl calls, there's no connection reuse — for that, use a persistent client like httpie or a script in Python/Node.
# curl reuses connections within a single invocation
# multiple URLs in one command share the TCP connection
curl -o a.txt https://example.com/a \
-o b.txt https://example.com/b
# keep-alive is on by default
# disable it for a single request
curl --no-keepalive https://example.com
# limit max connections (for multi-URL)
curl --max-connects 4 \
-O https://example.com/a -O https://example.com/b
# persistent session across shell calls (not natively supported)
# but you can use a config file to reduce overhead
curl -K session.conf https://example.com/a
curl -K session.conf https://example.com/b
# check if connection was reused
curl -v https://example.com 2>&1 | grep -i "Connection #0"HTTP/2 & HTTP/3
HTTP/2 multiplexes requests over one connection; HTTP/3 uses QUIC over UDP, eliminating head-of-line blocking. --http2 negotiates via ALPN over TLS; --http2-prior-knowledge skips negotiation (for cleartext h2c). --http3 requires a special curl build. Check %{http_version} to confirm what was actually used. Most modern CDNs support h2 by default.
# negotiate HTTP/2 (needs TLS + ALPN)
curl --http2 https://example.com
# force HTTP/2 (fail if not available)
curl --http2-only https://example.com
# negotiate HTTP/3 (QUIC, needs curl built with quiche/openssl-quic)
curl --http3 https://example.com
# force HTTP/3 only
curl --http3-only https://example.com
# check what version was negotiated
curl -s -o /dev/null -w "%{http_version}" https://example.com
echo # 2 or 3
# list supported HTTP versions
curl --version | grep -i -E "http2|http3"
# HTTP/2 with prior knowledge (no upgrade negotiation)
curl --http2-prior-knowledge http://example.comConditional Requests
Conditional requests save bandwidth: the server returns 304 Not Modified (no body) if the resource hasn't changed. -z (--time-cond) sets If-Modified-Since from a date or a local file's mtime. For ETag-based caching, set If-None-Match manually. If-Unmodified-Since enables optimistic concurrency — the PUT fails if someone else modified the resource.
# fetch only if modified since a date
curl -z "Wed, 21 Oct 2025 07:28:00 GMT" \
-O https://example.com/data.json
curl --timecond "Wed, 21 Oct 2025 07:28:00 GMT" \
-O https://example.com/data.json
# fetch only if changed (use a local file's mtime)
curl -z local_copy.json -O https://example.com/data.json
# ETag-based conditional (If-None-Match)
curl -H 'If-None-Match: "abc123"' \
-o data.json https://example.com/data.json
# server returns 304 Not Modified if ETag matches
# If-Modified-Since header manually
curl -H "If-Modified-Since: Wed, 21 Oct 2025 07:28:00 GMT" \
https://example.com/data.json
# If-Unmodified-Since (for concurrency control)
curl -X PUT -H "If-Unmodified-Since: Wed, 21 Oct 2025 07:28:00 GMT" \
-d '{"x":1}' https://api.example.com/dataInterface & DNS
--interface binds to a network card or IP — useful on multi-homed machines. --resolve pins a host to an IP, bypassing DNS and great for testing backends behind a load balancer. --doh-url uses DNS-over-HTTPS (privacy). -4/-6 force an IP family. Happy Eyeballs (default) tries both IPv4 and IPv6 to avoid IPv6 blackhole delays.
# bind to a specific network interface
curl --interface eth0 https://example.com
curl --interface 192.168.1.5 https://example.com
# pin a hostname to an IP (skip DNS)
curl --resolve example.com:443:1.2.3.4 https://example.com
# pin multiple hosts
curl --resolve example.com:443:1.2.3.4 \
--resolve api.example.com:443:1.2.3.5 \
https://example.com/
# use a custom DNS server (DoH)
curl --doh-url https://dns.google/dns-query https://example.com
curl --doh-url https://cloudflare-dns.com/dns-query https://example.com
# use a specific DNS resolver (plain DNS, curl 7.84+)
curl --dns-servers 8.8.8.8,1.1.1.1 https://example.com
# happy eyeballs: try IPv4 and IPv6 in parallel
curl --happy-eyeballs-timeout-ms 200 https://example.com
# force IPv4 or IPv6
curl -4 https://example.com
curl -6 https://example.comRange Requests & Partial
Range requests (-r) fetch part of a file, returning 206 Partial Content. Use cases: previewing large files, parallel multi-connection downloads (split into ranges, fetch concurrently, concatenate), and resuming interrupted transfers (-C -). The server must advertise Accept-Ranges: bytes. This is the basis of download accelerators like aria2.
# request a byte range (first 1024 bytes)
curl -r 0-1023 -o part.bin https://example.com/file.zip
# request from byte 1024 to end
curl -r 1024- -o rest.bin https://example.com/file.zip
# request last 500 bytes
curl -r -500 -o tail.bin https://example.com/file.zip
# check if server supports ranges
curl -I https://example.com/file.zip | grep -i accept-ranges
# download in parallel chunks
curl -r 0-49999999 -o part1.bin https://example.com/big.iso &
curl -r 50000000- -o part2.bin https://example.com/big.iso &
wait
cat part1.bin part2.bin > big.iso
# resume a partial download
curl -C - -o file.zip https://example.com/file.zip
# HTTP 206 = Partial Content (range succeeded)
curl -r 0-100 -v https://example.com/file.zip 2>&1 | grep HTTPExit Codes
Curl's exit codes distinguish failure modes: 6 (DNS), 7 (connection), 22 (HTTP error, only with -f), 28 (timeout), 35 (TLS), 60 (cert). Use these to build robust scripts that retry appropriately (e.g., retry on 28 but fail fast on 6). --fail-with-body returns the response body even on 4xx/5xx, making API error debugging far easier than -f.
# common curl exit codes:
# 0 - success
# 1 - unsupported protocol
# 3 - malformed URL
# 6 - couldn't resolve host (DNS failure)
# 7 - failed to connect
# 22 - HTTP error (use with -f)
# 26 - couldn't read a file (e.g., -d @missing.txt)
# 28 - operation timeout
# 35 - SSL connect error
# 47 - too many redirects
# 52 - server didn't reply anything
# 55 - failed sending network data
# 56 - failed receiving network data
# 60 - certificate verification failed
# check exit code
curl -sf https://example.com || echo "failed: $?"
# branch on specific codes
curl --max-time 5 https://example.com
case $? in
0) echo "ok" ;;
6) echo "DNS failure" ;;
7) echo "connection refused" ;;
28) echo "timeout" ;;
*) echo "error: $?" ;;
esac
# treat HTTP errors as failures
curl -f https://api.example.com || echo "HTTP error"
# still print body on HTTP error (7.76+)
curl --fail-with-body https://api.example.comSnippets curl associés
Copy-paste ready code for common tasks.
HTTP Methods
GET, POST, PUT, and DELETE requests.
Headers
Set and inspect request and response headers.
Authentication
Basic, bearer, and OAuth credentials.
Cookies
Send, save, and reuse cookies.
Upload and Download
Transfer files to and from a server.
Proxies
Route requests through HTTP or SOCKS proxies.
Follow Redirects
Chase 3xx responses automatically.
Debugging
Trace requests, timing, and transfers.
Was this helpful?