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
curlinstalled (curl --versionto confirm; install viasudo apt install curlorsudo 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:
jqinstalled 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:
| Feature | curl | wget |
|---|---|---|
| Primary use case | API calls, HTTP debugging, scripting | Recursive downloads, mirroring websites |
| REST API support | Full (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 required | Follows by default |
| Resume downloads | -C - | -c flag |
| Recursive download | Not supported | --recursive flag |
| Output to stdout | Default | Requires -O - |
| Scripting / piping | Excellent (stdout by default) | Less natural |
| Protocols supported | 30+ (FTP, SFTP, SMTP, IMAP…) | HTTP, HTTPS, FTP |
| Progress display | -# or --progress-bar | Shows 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
curlis the go-to Linux HTTP client for REST API testing, file transfers, and network debugging- Use
-Xto set the HTTP method (POST, PUT, PATCH, DELETE),-Hfor headers,-dfor request body - Authentication:
-H "Authorization: Bearer TOKEN"for token auth,-u user:passfor Basic Auth - Download files with
-O(original name) or-o filename; use-Lto follow redirects -vverbose 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-timeto make scripts production-resilient - Prefer curl over wget for API work and scripting; use wget for recursive site downloads
- Always use
--data-rawwhen body data contains literal@characters to avoid file-read confusion - Pipe to
jqfor formatted JSON output:curl -s https://api.example.com/data | jq .