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.