curl

Transfer data to and from a server

Updated 2026-07-05

curl sends an HTTP (or FTP, or a dozen other protocols') request and prints or saves whatever comes back. It's the tool behind "let me just check what this API returns," behind download scripts, behind health checks, and behind debugging why a webhook isn't firing.

The mental model: build a request, read a response

Every curl invocation assembles a request from pieces (method, URL, headers, body), sends it, and by default prints the response body to stdout. Everything else (status line, response headers, timing, progress) is opt-in via flags:

curl https://example.com              # GET, print the body
curl -I https://example.com           # HEAD, print only the response headers
curl -i https://example.com           # GET, print headers AND body
curl -v https://example.com           # GET, print the whole conversation (connect, TLS, headers, body)

-i/-I/-v are about what curl shows you, not what request it sends. A common confusion is meaning "make a HEAD request" but reaching for -i.

GET is implicit; other methods usually aren't

Without -X, curl sends GET, unless you give it a request body with -d/--data or -F/--form, in which case it switches to POST automatically. This is why you'll see curl -X POST url -d "..." written both with and without the explicit -X POST. The flag is often redundant but harmless, and worth keeping for readability. -X is not redundant for PUT or DELETE, which curl never infers.

-d vs -F: two different request bodies

  • -d / --data sends application/x-www-form-urlencoded data (or literally whatever string you give it, if you set Content-Type yourself for a JSON body).
  • -F / --form sends multipart/form-data, the format browsers use for <form> uploads, and the one you need for actually uploading a file (-F "file=@report.pdf").

Mixing these up is the single most common reason a "curl works but the server doesn't seem to see the data" bug happens. Check whether the endpoint expects urlencoded or multipart.

Exit codes tell scripts what actually happened

By default, curl exits 0 even for a 404 or 500 response. As far as curl is concerned, it successfully transferred something. Two flags change that:

  • -f / --fail makes curl exit non-zero (22) on HTTP error status codes, instead of printing the error page and exiting 0.
  • -m / --max-time bounds the whole operation; on timeout curl exits 28.

A script that checks curl's exit code without --fail is almost always checking the wrong thing. See Exit codes and error handling.

Redirects don't follow themselves

By default curl prints whatever the server sends back for a 3xx, including a redirect's near-empty body, without ever requesting the new location. Add -L / --location to follow redirects automatically. This trips up more people than it should, because the failure mode looks like "the page is just... blank" rather than an obvious error.

Getting machine-readable facts out of a response

-w / --write-out prints values from a template string after the transfer completes: status code, total time, effective URL after redirects, and more, without you having to parse curl's normal output:

curl -s -o /dev/null -w "%{http_code}\n" https://example.com

-o /dev/null throws away the body, -s silences the progress meter, and -w prints just the one number you asked for. This pattern, discard the body and print one templated fact, is the backbone of most health-check and monitoring scripts built on curl.

Saving output

-o file saves to a name you choose; -O saves using the remote URL's own filename (which means the URL needs an actual filename in its path; -O on https://example.com/ with no path fails, since there's nothing to name the file). -C - resumes an interrupted download instead of restarting from zero, when the server supports range requests.

Basic requests

GET is the default. This section covers what curl shows you by default and how to see more.

Fetch a URL and print the body

curl https://example.com

With no flags, curl sends a GET request and prints the response body to stdout.

Show output
<!doctype html><html><head><title>Example Domain</title>...

Fetch only the response headers

curl -I https://example.com

`-I` (`--head`) sends a HEAD request, same as GET but the server doesn't send a body, so you get headers only, fast.

Show output
HTTP/2 200
date: Sun, 05 Jul 2026 15:43:10 GMT
content-type: text/html
server: cloudflare
last-modified: Wed, 01 Jul 2026 17:52:37 GMT
allow: GET, HEAD

Print response headers AND body

curl -i https://httpbin.org/get

`-i` (`--include`) keeps the GET request but includes the response headers above the body in the output.

Show output
HTTP/2 200
content-type: application/json
content-length: 255

{
  "args": {},

Silence the progress meter

curl -s https://example.com -o page.html

`-s` (`--silent`) suppresses the progress bar, useful in scripts where you don't want that noise in logs.

Show both a response and any error, quietly

curl -sS https://example.com -o page.html

`-sS` keeps `-s`'s quiet progress but restores error messages `-s` alone would also hide. The combination you want in almost every script.

Saving output

Choosing a filename, or letting curl pick one.

Save using the remote file's own name

curl -O https://httpbin.org/robots.txt

`-O` (`--remote-name`) names the local file after the last path segment of the URL: here, `robots.txt`.

Show output
User-agent: *
Disallow: /deny

Resume an interrupted download

curl -C - -o robots.txt https://httpbin.org/robots.txt

`-C -` asks curl to figure out how much of the local file already exists and resume from there, if the server supports range requests. Avoids re-downloading a large file from scratch after a dropped connection.

Status codes and scripting

Making curl's exit code and output actually reflect success or failure.

Print only the HTTP status code

curl -s -o /dev/null -w "%{http_code}\n" https://example.com

Discard the body (`-o /dev/null`), silence the progress meter (`-s`), print just the status code (`-w`): the standard one-liner for a health check.

Show output
200

Get a 404 for a page that doesn't exist

curl -s -o /dev/null -w "%{http_code}\n" https://example.com/nonexistent

Without `--fail`, curl treats a 404 as a successfully completed transfer. It just happens to contain an error page.

Show output
404

Make curl itself fail on an HTTP error status

curl --fail -s https://example.com/nonexistent
echo "exit=$?"

`--fail` (`-f`) turns a 4xx/5xx response into a non-zero exit code (`22`) instead of curl reporting success. This is the flag a script's error handling almost always needs.

Show output
exit=22

Print several facts about a transfer at once

curl -sS -w "time_total=%{time_total}s\n" -o /dev/null https://example.com

`-w` accepts any combination of `%{...}` variables in one template string: status code, timing, sizes, effective URL, and more.

Show output
time_total=0.230857s

Redirects

curl doesn't follow redirects unless you tell it to.

See a redirect without following it

curl -s https://httpbin.org/redirect/1 -o /dev/null -w "%{http_code}\n"

Without `-L`, curl reports the redirect's own status code (`302`) and doesn't request the new location.

Show output
302

Follow redirects to the final destination

curl -Ls https://httpbin.org/redirect/1 -o /dev/null -w "%{http_code} %{url_effective}\n"

`-L` (`--location`) follows the `Location` header automatically; `%{url_effective}` shows where curl actually ended up.

Show output
200 https://httpbin.org/get

Sending data: POST, PUT, DELETE

GET is implicit; a request body usually switches curl to POST automatically.

Send form data with POST

curl -X POST https://httpbin.org/post -d "name=deb1"

`-d` sends `application/x-www-form-urlencoded` data; `-X POST` is technically redundant here since `-d` already implies POST, but it's clearer to read.

Show output
{
  "form": {
    "name": "deb1"
  }
}

POST without spelling out -X

curl https://httpbin.org/post -d "name=deb1"

Confirms the point above: `-d` alone is enough to make curl send POST instead of GET.

Show output
{'name': 'deb1'}

Send a JSON body

curl -X POST -H "Content-Type: application/json" -d '{"a":1}' https://httpbin.org/post

Set `Content-Type` yourself and pass raw JSON to `-d`. curl doesn't validate or reformat it, so make sure it's well-formed.

Show output
{'a': 1}

Send a query string alongside a GET request

curl -G --data-urlencode "q=hello world" https://httpbin.org/get

`-G` forces curl back to GET while still using `--data-urlencode` to build and properly escape the query string. Otherwise `-d`'s data would go in the request body instead of the URL.

Show output
{'q': 'hello world'}

Update a resource with PUT

curl -X PUT https://httpbin.org/put -d "status=updated"

Unlike POST, curl never infers PUT. `-X PUT` is required.

Show output
{'status': 'updated'}

Upload a file as multipart form data

curl -F "file=@robots.txt" https://httpbin.org/post

`-F` (`--form`) sends `multipart/form-data`, the format needed to actually upload a file. `-d` can't do this. The `@` prefix tells curl to read the named file's contents rather than sending the literal string.

Headers and identity

Adding request headers, and reading back what the server received.

Add a custom request header

curl -H "X-Test: yes" https://httpbin.org/headers

`-H` adds one header per flag; repeat it for multiple headers.

Show output
{
  "headers": {
    "X-Test": "yes"
  }
}

Set a custom User-Agent

curl -A "MyScript/1.0" https://httpbin.org/user-agent

`-A` (`--user-agent`) is shorthand for `-H "User-Agent: ..."`. Some APIs reject requests with curl's default user agent, so this comes up more often than you'd expect.

Show output
{
  "user-agent": "MyScript/1.0"
}

Authentication and cookies

Basic auth and session cookies.

Authenticate with HTTP Basic auth

curl -u user:pass https://httpbin.org/basic-auth/user/pass

`-u user:pass` sends the `Authorization: Basic ...` header curl builds from the credentials. Never hardcode real credentials in a script; read them from an environment variable or a credentials file instead.

Show output
{
  "authenticated": true,
  "user": "user"
}

See what an unauthenticated request gets

curl -s -o /dev/null -w "%{http_code}\n" https://httpbin.org/basic-auth/user/pass

Without `-u`, the same endpoint returns `401 Unauthorized`.

Show output
401

Save cookies from a response

curl -c cookies.txt https://httpbin.org/cookies/set/session/abc123

`-c` (`--cookie-jar`) writes any `Set-Cookie` headers to a file in Netscape cookie-file format.

Show output
# Netscape HTTP Cookie File
httpbin.org	FALSE	/	FALSE	0	session	abc123

Send cookies from a saved file

curl -b cookies.txt https://httpbin.org/cookies

`-b` (`--cookie`) reads a cookie jar and sends its cookies with the request. Pair with `-c` to persist a session across multiple curl calls, the way a browser would.

Show output
{
  "cookies": {
    "session": "abc123"
  }
}

Timeouts, retries, and TLS

Making curl behave well against a slow, flaky, or self-signed endpoint.

Give up after a fixed time

curl -m 2 https://httpbin.org/delay/5 -o /dev/null
echo "exit=$?"

`-m` (`--max-time`) bounds the entire request; curl exits `28` on timeout. A script should treat that distinctly from a clean HTTP error.

Show output
exit=28

Retry a failing request automatically

curl --retry 2 --retry-delay 1 -o /dev/null -s -w "%{http_code}\n" https://httpbin.org/status/500

`--retry` retries on transient failures and select 5xx responses, waiting `--retry-delay` seconds between attempts. Useful against flaky upstreams, not a substitute for `--fail` in the exit-code check.

Show output
500

Skip TLS certificate verification

curl -sk https://example.com -o /dev/null -w "%{http_code}\n"

`-k` (`--insecure`) disables certificate checks. Fine for a self-signed cert on a box you control during setup, never for anything handling real traffic or credentials.

Show output
200

Watch the full TLS handshake and request

curl -v https://example.com -o /dev/null

`-v` (`--verbose`) prints the connection, TLS handshake, and every header sent and received. The first thing to reach for when a request behaves unexpectedly.

Show output
* Host example.com:443 was resolved.
* IPv4: 104.20.23.154, 172.66.147.243
*   Trying 104.20.23.154:443...
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/ssl/certs/ca-certificates.crt

Practical scripting patterns

curl calls that show up inside larger scripts.

Check whether a site is up before proceeding

if curl --fail -s -o /dev/null https://example.com; then
  echo "site is up"
fi

`--fail` plus `-s -o /dev/null` reduces the whole request to a plain yes/no exit code, ready for an `if`.

Show output
site is up

Extract one field from a JSON response

curl -s https://httpbin.org/get | python3 -c "import json,sys; print(json.load(sys.stdin)['url'])"

curl doesn't parse JSON itself. Pipe into `python3 -c` (or `jq` if it's installed) to pull out a specific field.

Show output
https://httpbin.org/get

See the public IP a request would come from

curl -s https://icanhazip.com

A quick way to confirm which outbound IP a script, container, or VPN is actually using. Hostnames/IPs shown here are sanitised.

Show output
203.0.113.42