The curl command Linux users rely on daily is far more than a download tool — it is a complete HTTP client capable of testing REST APIs, automating workflows, debugging network issues, and transferring data over dozens of protocols. Whether you are troubleshooting a broken webhook, scripting an API integration, or inspecting TLS certificates, mastering curl is one of the highest-leverage skills a sysadmin or developer can have. This guide walks through every practical use case from simple GETs to full CRUD REST API testing, with real-world examples throughout.

Prerequisites

  • A Linux system with curl installed (curl --version to confirm; install via sudo apt install curl or sudo dnf install curl)
  • Basic familiarity with the terminal and HTTP concepts (request methods, headers, status codes)
  • An API endpoint to test against — the examples use https://jsonplaceholder.typicode.com, a free public mock API
  • Optional: jq installed for pretty-printing JSON responses (sudo apt install jq)

Making HTTP Requests: GET, POST, PUT, DELETE

curl defaults to GET, so the simplest possible command is just a URL:

curl https://jsonplaceholder.typicode.com/posts/1

Add -s (silent) to suppress the progress meter in scripts, and pipe to jq for readable output:

curl -s https://jsonplaceholder.typicode.com/posts/1 | jq .

POST — creating a resource:

curl -s -X POST \
  -H "Content-Type: application/json" \
  -d '{"title":"My Post","body":"Hello world","userId":1}' \
  https://jsonplaceholder.typicode.com/posts | jq .

The -X POST flag sets the method. -H adds a header. -d sends the request body. For form submissions instead of JSON, omit the Content-Type header or use -d "field=value&other=data".

PUT — updating a resource:

curl -s -X PUT \
  -H "Content-Type: application/json" \
  -d '{"title":"Updated Title","body":"New body","userId":1}' \
  https://jsonplaceholder.typicode.com/posts/1 | jq .

PATCH — partial update:

curl -s -X PATCH \
  -H "Content-Type: application/json" \
  -d '{"title":"Just the title changed"}' \
  https://jsonplaceholder.typicode.com/posts/1 | jq .

DELETE — removing a resource:

curl -s -X DELETE https://jsonplaceholder.typicode.com/posts/1
echo "HTTP status: $?"

To capture the HTTP status code explicitly:

curl -s -o /dev/null -w "%{http_code}" -X DELETE \
  https://jsonplaceholder.typicode.com/posts/1

The -w (write-out) flag with %{http_code} outputs only the numeric status code — ideal for checking success or failure in shell scripts.

Authentication: Headers, Bearer Tokens, and Basic Auth

Most production APIs require authentication. curl handles every common scheme.

Bearer token (OAuth 2.0 / JWT):

curl -s -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  https://api.example.com/protected-resource | jq .

HTTP Basic Auth — two equivalent forms:

# Form 1: -u flag (curl adds the Authorization header automatically)
curl -s -u myuser:mysecretpass https://api.example.com/data

# Form 2: explicit header (useful when scripting with variables)
curl -s -H "Authorization: Basic $(echo -n myuser:mysecretpass | base64)" \
  https://api.example.com/data

API key as a query parameter:

curl -s "https://api.example.com/data?api_key=YOUR_KEY_HERE" | jq .

API key as a custom header (common with services like OpenAI or Stripe):

curl -s -H "X-API-Key: YOUR_KEY_HERE" https://api.example.com/endpoint | jq .

Storing credentials in a netrc file keeps secrets out of shell history:

# ~/.netrc
machine api.example.com
login myuser
password mysecretpass
curl -s --netrc https://api.example.com/data

File Download and Upload

Basic download — save with original filename:

curl -O https://releases.ubuntu.com/24.04/ubuntu-24.04-desktop-amd64.iso

Download to a specific path:

curl -o /tmp/ubuntu.iso https://releases.ubuntu.com/24.04/ubuntu-24.04-desktop-amd64.iso

Follow redirects (many download URLs redirect):

curl -L -O https://github.com/cli/cli/releases/latest/download/gh_linux_amd64.tar.gz

Resume an interrupted download:

curl -C - -O https://example.com/large-file.tar.gz

Download with progress bar (useful in interactive terminals):

curl --progress-bar -O https://example.com/large-file.tar.gz

Upload a file with multipart/form-data (simulating a browser file input):

curl -s -X POST \
  -F "file=@/path/to/document.pdf" \
  -F "description=My document" \
  https://api.example.com/upload | jq .

Upload raw binary with PUT:

curl -s -X PUT \
  -H "Content-Type: application/octet-stream" \
  --data-binary @/path/to/image.png \
  https://api.example.com/files/image.png

Verbose Debugging with -v and —trace

The -v flag is your best debugging tool. It prints the TLS handshake, request headers, response headers, and status line:

curl -v https://jsonplaceholder.typicode.com/posts/1

Sample output (truncated):

* Connected to jsonplaceholder.typicode.com (104.21.x.x) port 443
* TLSv1.3, TLS handshake
> GET /posts/1 HTTP/2
> Host: jsonplaceholder.typicode.com
> User-Agent: curl/8.5.0
> Accept: */*
>
< HTTP/2 200
< content-type: application/json; charset=utf-8
< cache-control: max-age=43200
<
{ ... JSON body ... }

Lines starting with > are sent by curl; < are received from the server. This is invaluable for verifying that your headers are being sent correctly.

For even more detail, use --trace to dump raw bytes:

curl --trace /tmp/curl-trace.txt https://api.example.com/endpoint

Check only headers without downloading the body:

curl -I https://example.com

-I sends a HEAD request. To send HEAD while still specifying a custom method:

curl -s -X HEAD -I https://example.com

Inspect TLS certificate information:

curl -v --connect-to :: https://example.com 2>&1 | grep -A5 "Server certificate"

Or with --cert-status on supported builds.

curl vs wget: Choosing the Right Tool

Both curl and wget download files, but they serve different primary purposes:

Featurecurlwget
Primary use caseAPI calls, HTTP debugging, scriptingRecursive downloads, mirroring websites
REST API supportFull (GET/POST/PUT/DELETE/PATCH)GET only by default
JSON request body-d '{"key":"val"}'Not natively supported
Custom headers-H "Header: Value"--header="Header: Value"
Follow redirects-L flag requiredFollows by default
Resume downloads-C --c flag
Recursive downloadNot supported--recursive flag
Output to stdoutDefaultRequires -O -
Scripting / pipingExcellent (stdout by default)Less natural
Protocols supported30+ (FTP, SFTP, SMTP, IMAP…)HTTP, HTTPS, FTP
Progress display-# or --progress-barShows by default

Rule of thumb: Use curl for API work, debugging, and scripting pipelines. Use wget when you need to mirror or recursively download a website, or when resuming downloads in a simpler invocation.

Real-World Scenario: Testing a REST API End-to-End

You have a production server running a new user management microservice. Before deploying, you want to validate the full CRUD lifecycle from the command line, capturing status codes for CI/CD integration.

#!/bin/bash
set -euo pipefail
BASE="https://api.example.com/v1"
TOKEN="eyJhbGciOiJIUzI1NiIs..."

echo "=== Testing User Management API ==="

# 1. Health check
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/health")
echo "Health check: $STATUS"
[[ "$STATUS" == "200" ]] || { echo "FAIL: API not healthy"; exit 1; }

# 2. Create a new user
RESPONSE=$(curl -s -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"username":"testuser","email":"test@example.com","role":"viewer"}' \
  "$BASE/users")
USER_ID=$(echo "$RESPONSE" | jq -r '.id')
echo "Created user ID: $USER_ID"

# 3. Retrieve the user
curl -s -H "Authorization: Bearer $TOKEN" \
  "$BASE/users/$USER_ID" | jq '.username'

# 4. Update the user role
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
  -X PATCH \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"role":"editor"}' \
  "$BASE/users/$USER_ID")
echo "PATCH status: $STATUS"
[[ "$STATUS" == "200" ]] || echo "WARNING: expected 200, got $STATUS"

# 5. List all users and count
COUNT=$(curl -s -H "Authorization: Bearer $TOKEN" \
  "$BASE/users" | jq 'length')
echo "Total users: $COUNT"

# 6. Delete test user
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
  -X DELETE \
  -H "Authorization: Bearer $TOKEN" \
  "$BASE/users/$USER_ID")
echo "DELETE status: $STATUS"
[[ "$STATUS" == "204" ]] || echo "WARNING: expected 204, got $STATUS"

echo "=== All tests passed ==="

This script validates the entire API contract and exits with a non-zero code if anything fails — ready to drop into a CI/CD pipeline.

Gotchas and Edge Cases

SSL certificate errors on internal/dev servers: Use -k or --insecure to skip verification, but only in development:

curl -k https://internal-dev-server.local/api

Single quotes vs double quotes in shells: Inside double quotes, $ and backticks are expanded. Use single quotes around JSON bodies to avoid surprises:

# Safe: single quotes prevent variable expansion inside JSON
curl -d '{"userId":1}' https://api.example.com/items

# Risky: $HOME gets expanded inside double quotes
curl -d "{\"userId\":$USER_ID}" https://api.example.com/items  # OK if $USER_ID is intentional

Sending a literal @ in POST data: curl treats -d @filename as “read body from file”. To send a literal @, use --data-raw:

curl --data-raw '{"email":"user@example.com"}' https://api.example.com/subscribe

Rate limiting and retries: Add --retry and --retry-delay for resilient scripts:

curl --retry 5 --retry-delay 2 --retry-all-errors \
  -s https://api.example.com/endpoint | jq .

Timeout control: Prevent scripts from hanging forever:

curl --connect-timeout 10 --max-time 30 https://api.example.com/slow-endpoint

--connect-timeout limits the connection phase; --max-time caps total transfer time.

Cookies: Send and persist cookies for session-based APIs:

# Save cookies to a jar after login
curl -s -c /tmp/cookies.txt -X POST \
  -d "username=admin&password=secret" \
  https://example.com/login

# Reuse saved cookies for authenticated requests
curl -s -b /tmp/cookies.txt https://example.com/dashboard

Troubleshooting Common Issues

“Could not resolve host”: DNS failure. Check /etc/resolv.conf or test with curl --dns-servers 8.8.8.8 https://example.com.

“SSL: no alternative certificate subject name matches”: The server certificate does not match the hostname. Use -v to see what the certificate says; the hostname in your URL may be wrong.

“Empty reply from server”: The server closed the connection without sending headers. Try -v to see if the TLS handshake succeeded; may indicate a protocol mismatch (--http1.1 flag can help).

Response body is empty but status is 200: Some endpoints return 204 No Content on success (especially DELETE). Use -v to confirm the status line.

Large response truncated in terminal: Pipe to less or redirect to a file:

curl -s https://api.example.com/large-dataset | jq . | less
curl -s https://api.example.com/large-dataset > /tmp/response.json

Checking which version of curl you have and what protocols it supports:

curl --version

Look for Protocols: in the output — this shows FTP, SFTP, HTTP/2, HTTP/3 availability depending on your build.

Summary

  • curl is the go-to Linux HTTP client for REST API testing, file transfers, and network debugging
  • Use -X to set the HTTP method (POST, PUT, PATCH, DELETE), -H for headers, -d for request body
  • Authentication: -H "Authorization: Bearer TOKEN" for token auth, -u user:pass for Basic Auth
  • Download files with -O (original name) or -o filename; use -L to follow redirects
  • -v verbose mode shows full request/response headers and TLS details — essential for debugging
  • -w "%{http_code}" extracts the status code for scripting and CI/CD validation
  • Use --retry, --connect-timeout, and --max-time to make scripts production-resilient
  • Prefer curl over wget for API work and scripting; use wget for recursive site downloads
  • Always use --data-raw when body data contains literal @ characters to avoid file-read confusion
  • Pipe to jq for formatted JSON output: curl -s https://api.example.com/data | jq .