BytePane

curl Command Guide: HTTP Requests, Headers & API Testing

CLI Tools19 min read

The scenario: You're integrating a third-party API. Their docs show a curl command with eight flags you don't recognize. You copy-paste it, it works, and you move on — until it doesn't work in staging, and you have no idea why. This guide explains every flag, every edge case, so you can read and write curl commands cold.

Key Takeaways

  • curl is installed on 10 billion+ devices — per the curl project's own count, making it the most widely deployed HTTP client in existence.
  • -d implies POST — you can omit -X POST when sending a body with -d.
  • -v vs -w: use -v for interactive debugging, -w for scripted metrics (status codes, latency).
  • -L does not preserve method by default — a POSTed request that gets 301-redirected becomes a GET unless you explicitly prevent it.
  • Never use -k in production scripts — skipping TLS verification defeats the entire point of HTTPS.

What curl Is (and Isn't)

curl (originally Client for URLs, written by Daniel Stenberg in 1998) is a command-line tool and library for transferring data over URLs. It speaks 28 protocols — HTTP/1.1, HTTP/2, HTTP/3, FTP, SFTP, SMTP, WebSocket — but its primary use case in 2026 is HTTP API testing and scripting.

According to the curl project's installation statistics, curl ships in macOS, most Linux distributions, Windows 10+, Android, iOS, and thousands of embedded systems — giving it an estimated 10 billion+ deployment count. The Stack Overflow Developer Survey 2024 lists it as one of the top command-line tools used by developers globally. It is not a GUI, not a full API client (that's Postman's job), and not wget — though the two overlap on basic downloads.

The Anatomy of a curl Command

curl [OPTIONS] [URL]

# Minimal GET request
curl https://api.example.com/users

# The most complete form you'll see in API docs:
curl \
  --request POST \                              # HTTP method
  --url https://api.example.com/v1/orders \    # Target URL
  --header 'Content-Type: application/json' \ # Request header
  --header 'Authorization: Bearer $TOKEN' \   # Auth header
  --data '{"item":"widget","qty":3}' \         # Request body
  --silent \                                   # Suppress progress meter
  --fail \                                     # Exit non-zero on HTTP errors
  --output response.json \                     # Write response to file
  --write-out '%{http_code}\n'                 # Print status code to stdout

# Short-form equivalent:
curl -X POST https://api.example.com/v1/orders \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer $TOKEN' \
  -d '{"item":"widget","qty":3}' \
  -sSf -o response.json -w '%{http_code}\n'

GET Requests: Fundamentals and Query Strings

# Basic GET — no flag needed, GET is default
curl https://httpbin.org/get

# With query parameters — URL-encode manually or use --data-urlencode
curl 'https://api.example.com/search?q=hello+world&limit=10'

# --get (-G) + --data-urlencode: curl handles encoding for you
curl -G https://api.example.com/search \
  --data-urlencode 'q=hello world' \
  --data-urlencode 'filter=status:active'
# Sends: GET /search?q=hello%20world&filter=status%3Aactive

# Accept specific content type
curl -H 'Accept: application/json' https://api.example.com/users

# Custom User-Agent (some APIs block curl's default agent)
curl -A 'MyApp/2.0' https://api.example.com/data

# Inspect response headers only (no body)
curl -I https://api.example.com/health
# -I sends HEAD request — response has headers, no body
# Useful for checking Cache-Control, Content-Type, ETag

# Show both headers and body (append -i to any request)
curl -i https://api.example.com/health

When inspecting response headers, look for X-RateLimit-Remaining, Retry-After, and X-Request-ID — these are critical for debugging rate limiting and distributed tracing. See the HTTP Status Codes guide for what each response code means.

POST, PUT, PATCH, DELETE: Sending Data

# POST with JSON body — -d implies POST, -X POST is redundant here
curl https://api.example.com/users \
  -H 'Content-Type: application/json' \
  -d '{"name":"Alice","email":"[email protected]"}'

# POST from a JSON file — @ prefix reads file contents
curl https://api.example.com/users \
  -H 'Content-Type: application/json' \
  -d @user.json

# POST form data (application/x-www-form-urlencoded — default for -d)
curl https://api.example.com/login \
  -d 'username=alice&password=secret'

# POST multipart form (file upload — sets Content-Type: multipart/form-data automatically)
curl https://api.example.com/upload \
  -F '[email protected]' \
  -F 'caption=Profile photo' \
  -F 'user_id=123'

# Upload with specific MIME type override
curl https://api.example.com/documents \
  -F '[email protected];type=application/pdf'

# PUT — full resource replacement
curl -X PUT https://api.example.com/users/123 \
  -H 'Content-Type: application/json' \
  -d '{"name":"Alice Smith","email":"[email protected]","role":"admin"}'

# PATCH — partial update
curl -X PATCH https://api.example.com/users/123 \
  -H 'Content-Type: application/json' \
  -d '{"role":"admin"}'

# DELETE
curl -X DELETE https://api.example.com/users/123

# DELETE with body (unusual but some APIs require it)
curl -X DELETE https://api.example.com/items \
  -H 'Content-Type: application/json' \
  -d '{"ids":[1,2,3]}'

Authentication: Every Method You'll Encounter

curl supports every authentication scheme used in modern APIs. Per Postman's 2024 State of the API report, 98% of APIs require some form of authentication. Here's how to provide each type from the command line.

# API Key in header (most common)
curl -H 'X-API-Key: your_api_key_here' https://api.example.com/data

# Bearer token (OAuth 2.0 / JWT)
curl -H 'Authorization: Bearer eyJhbGci...' https://api.example.com/data

# Basic auth — curl encodes user:pass as Base64 automatically
curl -u alice:secretpassword https://api.example.com/data
# Equivalent to: -H 'Authorization: Basic YWxpY2U6c2VjcmV0cGFzc3dvcmQ='

# Basic auth — password prompt (never put passwords in commands)
curl -u alice https://api.example.com/data
# curl prompts for password — doesn't appear in shell history

# Digest auth
curl --digest -u alice:secret https://api.example.com/data

# API key in query string (less secure — appears in server logs)
curl 'https://api.example.com/data?api_key=your_key'

# OAuth 1.0a — curl doesn't support natively; use oauth tool
curl -H "Authorization: OAuth oauth_consumer_key=...,oauth_token=..." \
  https://api.example.com/data

# Client certificate (mTLS)
curl --cert client.crt --key client.key \
  --cacert ca.crt \
  https://api.example.com/data

# Netrc file — store credentials without exposing them in commands
# ~/.netrc format:
# machine api.example.com
#   login alice
#   password secretpassword
curl --netrc https://api.example.com/data  # Reads from ~/.netrc

# Store token in env var — prevents it appearing in process list
export API_TOKEN="eyJhbGci..."
curl -H "Authorization: Bearer $API_TOKEN" https://api.example.com/data

For a deeper comparison of authentication methods including JWT structure and OAuth flows, see the API Authentication Methods guide and the JWT Tokens Explained article.

Debugging: -v, -i, --trace, and -w

Knowing which debugging flag to use for which problem saves significant time. The four flags serve different purposes and aren't interchangeable.

# -i: Show response headers before body (non-verbose)
curl -i https://httpbin.org/get
# HTTP/2 200
# content-type: application/json
# ...followed by body...

# -v: Full conversation log (request + response headers + TLS info)
curl -v https://httpbin.org/get
# * Connected to httpbin.org (34.227.213.82) port 443
# * SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
# > GET /get HTTP/2
# > Host: httpbin.org
# > User-Agent: curl/8.7.1
# > Accept: */*
# >
# < HTTP/2 200
# < content-type: application/json
# ...body...

# --trace: Byte-level hex dump (debugging binary protocols, WebSockets)
curl --trace trace.txt https://httpbin.org/get
curl --trace - https://httpbin.org/get  # Print trace to stdout

# -w: Scripted metrics — prints formatted data after transfer
# Great for health checks and latency testing
curl -s -o /dev/null -w '%{http_code}\n' https://api.example.com/health
# 200

# Full timing breakdown
curl -s -o /dev/null -w \
  'DNS: %{time_namelookup}s\nConnect: %{time_connect}s\nTLS: %{time_appconnect}s\nTTFB: %{time_starttransfer}s\nTotal: %{time_total}s\n' \
  https://api.example.com
# DNS:     0.018s
# Connect: 0.054s
# TLS:     0.121s
# TTFB:    0.243s
# Total:   0.251s

# -w variables reference:
# %{http_code}        — HTTP status code (200, 404, 500...)
# %{time_total}       — Total transfer time in seconds
# %{time_connect}     — Time to TCP connect
# %{time_appconnect}  — Time to TLS handshake completion
# %{time_starttransfer} — Time to first byte (TTFB)
# %{size_download}    — Response body size in bytes
# %{speed_download}   — Average download speed (bytes/sec)
# %{url_effective}    — Final URL after redirects
# %{redirect_count}   — Number of redirects followed
# %{num_connects}     — Number of new connections made

TLS, Certificates, and the -k Flag You Should Never Use in CI

# Default: curl verifies TLS certificates
curl https://api.example.com/data  # Fails if cert is invalid/self-signed

# -k (--insecure): Skip TLS verification — DEVELOPMENT ONLY
curl -k https://localhost:8443/api
# NEVER use in production scripts — defeats the point of HTTPS
# CI pipelines: if you're tempted to use -k, fix the certificate instead

# Trust a specific CA certificate (for internal/staging APIs)
curl --cacert /path/to/internal-ca.crt https://internal-api.company.com/data

# Specify exact server certificate to pin against
curl --pinnedpubkey sha256//AAAAAAA...= https://api.example.com/data

# Client certificates (mutual TLS)
curl --cert client.pem --key client.key https://api.example.com/data

# Separate cert and key files
curl --cert /certs/client.crt --key /certs/client.key \
  --cacert /certs/ca-bundle.crt \
  https://api.example.com/data

# Specify TLS version (useful for debugging compatibility)
curl --tls-max 1.2 https://api.example.com/data     # Limit to TLS 1.2
curl --tlsv1.3 https://api.example.com/data          # Require at least TLS 1.3

# Check certificate expiry
curl -vI https://api.example.com/health 2>&1 | grep -E 'expire|issue'
# *  SSL certificate verify ok.
# *  subject: CN=api.example.com
# *  start date: Jan  1 00:00:00 2026 GMT
# *  expire date: Jan  1 00:00:00 2027 GMT

Cookies, Sessions, and State

# Send a specific cookie
curl -b 'session_id=abc123; theme=dark' https://app.example.com/dashboard

# Save cookies from response to a file
curl -c cookies.txt https://app.example.com/login

# Load cookies from file AND save new cookies back to it
# This maintains a session across multiple requests
curl -b cookies.txt -c cookies.txt \
  -X POST https://app.example.com/login \
  -d 'username=alice&password=secret'

# Use the session for subsequent requests
curl -b cookies.txt https://app.example.com/dashboard

# Complete login flow example
# Step 1: Login and capture session cookie
curl -s -c session.jar \
  -X POST https://app.example.com/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"[email protected]","password":"secret"}' \
  | python3 -m json.tool  # Pretty-print response

# Step 2: Use session for authenticated request
curl -s -b session.jar \
  https://app.example.com/api/profile \
  | python3 -m json.tool

# Delete a specific cookie
curl -b 'session_id=; expires=Thu, 01 Jan 1970 00:00:00 GMT' \
  https://app.example.com/logout

Redirects, Retries, and Timeouts

# Follow redirects (-L / --location)
curl -L https://short.url/abc  # Follows to final destination

# Limit redirect depth
curl -L --max-redirs 5 https://example.com

# Follow redirects and preserve POST method (don't convert to GET on 301)
curl -L --post301 --post302 \
  -X POST https://api.example.com/v1/orders \
  -d '{"item":"widget"}'

# Show final URL after redirects
curl -s -o /dev/null -w '%{url_effective}\n' -L https://bit.ly/abc

# Connection timeout — how long to wait for a TCP connection
curl --connect-timeout 5 https://api.example.com/data

# Total operation timeout — including transfer time
curl --max-time 30 https://api.example.com/large-export

# Retry on failure (transient network errors, not HTTP errors)
curl --retry 3 --retry-delay 2 https://api.example.com/data
# Waits 2 seconds between retries

# Retry with exponential backoff
curl --retry 5 --retry-delay 1 --retry-max-time 60 https://api.example.com/data

# Exit with non-zero status on HTTP errors (4xx, 5xx)
# Without -f, curl exits 0 even if server returns 404
curl -f https://api.example.com/data || echo "Request failed"
# -fsSL is a common CI idiom: fail-fast + silent + follow redirects
curl -fsSL https://api.example.com/health | jq .status

HTTP/2 and HTTP/3

# Force HTTP/2 (requires curl built with nghttp2)
curl --http2 https://api.example.com/data

# Force HTTP/1.1 (useful for debugging protocol-specific issues)
curl --http1.1 https://api.example.com/data

# HTTP/3 (QUIC) — requires curl 7.66+ with quiche or ngtcp2
curl --http3 https://api.example.com/data

# Check which protocol was used
curl -v https://api.example.com/data 2>&1 | grep 'HTTP/'
# * Using HTTP2, server supports multiplexing

# HTTP/2 multiplexing: multiple requests over one connection
# curl handles this automatically with --http2 when the server supports it
# Visible in -v output: "Re-using existing connection"

# Detect if a server supports HTTP/2
curl -sI --http2 https://api.example.com/ | grep -i 'HTTP/'
# HTTP/2 200  — server accepted HTTP/2
# HTTP/1.1 200 OK — server downgraded to HTTP/1.1

Scripting Patterns for CI/CD and Automation

curl is a first-class tool in CI/CD pipelines. These patterns extract structured output reliably, handle errors, and integrate with tools like jq.

#!/bin/bash

# Pattern 1: Health check with status code validation
STATUS=$(curl -s -o /dev/null -w '%{http_code}' https://api.example.com/health)
if [ "$STATUS" != "200" ]; then
  echo "Health check failed: HTTP $STATUS"
  exit 1
fi

# Pattern 2: Capture response body AND status code separately
HTTP_RESPONSE=$(curl -s -w '\n%{http_code}' https://api.example.com/users)
HTTP_BODY=$(echo "$HTTP_RESPONSE" | head -n -1)
HTTP_STATUS=$(echo "$HTTP_RESPONSE" | tail -n 1)

# Pattern 3: Extract a JSON field with jq
TOKEN=$(curl -s -X POST https://api.example.com/auth/token \
  -H 'Content-Type: application/json' \
  -d '{"client_id":"app","client_secret":"secret"}' \
  | jq -r '.access_token')

# Pattern 4: Paginate through results
PAGE=1
while true; do
  RESPONSE=$(curl -s "https://api.example.com/items?page=$PAGE&per_page=100" \
    -H "Authorization: Bearer $TOKEN")

  # Process items
  echo "$RESPONSE" | jq '.items[]'

  # Check if there's a next page
  HAS_NEXT=$(echo "$RESPONSE" | jq -r '.pagination.has_next')
  [ "$HAS_NEXT" = "true" ] || break
  PAGE=$((PAGE + 1))
done

# Pattern 5: Webhook testing — send and verify
curl -X POST https://webhook.site/your-uuid \
  -H 'Content-Type: application/json' \
  -H 'X-Webhook-Secret: shared_secret' \
  -d @payload.json \
  -w '\nStatus: %{http_code}\n'

# Pattern 6: Upload a file and poll for processing status
UPLOAD_ID=$(curl -s -X POST https://api.example.com/jobs \
  -F '[email protected]' | jq -r '.job_id')

until [ "$(curl -s "https://api.example.com/jobs/$UPLOAD_ID" | jq -r '.status')" = "complete" ]; do
  echo "Waiting for job $UPLOAD_ID..."
  sleep 5
done

curl vs HTTPie vs wget: When to Use Each

FeaturecurlHTTPiewget
Installed by defaultmacOS, Linux, Windows 10+No (pip install httpie)Most Linux distros
JSON ergonomicsManual (-H + -d)Auto JSON (key=value)Poor
Colored outputNoYes (syntax highlighted)No
Scripting reliabilityExcellent (-w, -f, --retry)GoodLimited
File downloadFull-featured (-o, --range)BasicExcellent (--mirror, recursive)
Protocol support28 protocolsHTTP/HTTPS onlyHTTP, HTTPS, FTP
Best use caseScripting, CI, API testingInteractive API explorationRecursive site download

In CI/CD pipelines, always prefer curl — it's universally available, has stable flag semantics across versions, and its -w flag makes it trivial to extract structured metrics. HTTPie is excellent for interactive exploration and produces human-readable output, but shouldn't be a CI dependency.

The 20 Flags You'll Actually Use

FlagLong formPurpose
-X--requestSet HTTP method (GET, POST, PUT, PATCH, DELETE)
-H--headerAdd request header (repeatable)
-d--dataRequest body; @filename reads from file
-F--formMultipart form data (file uploads)
-u--userBasic auth user:password
-i--includeInclude response headers in output
-I--headSend HEAD request (headers only)
-v--verboseFull request/response + TLS conversation
-s--silentSuppress progress meter and errors
-S--show-errorShow errors even when -s is set (use -sS together)
-f--failExit non-zero on HTTP 4xx/5xx responses
-L--locationFollow HTTP redirects (301, 302, etc.)
-o--outputWrite response body to file
-O--remote-nameSave to filename from URL
-w--write-outPrint formatted metrics after transfer
-b--cookieSend cookie string or load from file
-c--cookie-jarSave received cookies to file
-k--insecureSkip TLS verification (dev only!)
--retry--retry NRetry N times on transient failures
--max-time--max-time NTotal time limit in seconds

Practical Examples: Real API Workflows

# GitHub API — list repos for a user
curl -s \
  -H 'Accept: application/vnd.github+json' \
  -H "Authorization: Bearer $GITHUB_TOKEN" \
  'https://api.github.com/users/torvalds/repos?per_page=5&sort=stars' \
  | jq '.[].full_name'

# Stripe API — create a payment intent
curl -s https://api.stripe.com/v1/payment_intents \
  -u "$STRIPE_SECRET_KEY:" \
  -d amount=2000 \
  -d currency=usd \
  -d 'payment_method_types[]=card'

# AWS S3 — pre-signed URL upload (no auth header needed)
curl -X PUT 'https://bucket.s3.amazonaws.com/file.txt?AWSAccessKeyId=...&Expires=...&Signature=...' \
  -H 'Content-Type: text/plain' \
  --upload-file ./file.txt

# OpenAI API — chat completion
curl https://api.openai.com/v1/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "gpt-4o",
    "messages": [{"role":"user","content":"Explain curl in one sentence"}],
    "max_tokens": 100
  }' | jq -r '.choices[0].message.content'

# Slack webhook — post a message
curl -X POST "$SLACK_WEBHOOK_URL" \
  -H 'Content-Type: application/json' \
  -d '{"text":"Deployment complete :rocket:"}'

# Test a locally running service
curl -s localhost:3000/api/health | jq .
curl -s localhost:3000/api/users \
  -H 'Authorization: Bearer test-token' | jq '.users | length'

When testing REST APIs, understanding the response status codes is as important as the request construction. The REST API Best Practices guide covers status code semantics, versioning, and error response formats in depth.

Frequently Asked Questions

What does curl -X POST actually do?

-X (--request) explicitly sets the HTTP method. For POST with a body you can usually omit it — curl infers POST when you use -d. Use -X explicitly for PUT, PATCH, DELETE, OPTIONS. Gotcha: -X POST persists across redirects. If a server 301-redirects a POST, curl re-sends POST to the new URL. Use --post301 or --post302 to preserve method intentionally.

What is the difference between curl -d and --data-raw?

-d (--data) treats values starting with @ as filenames to read: -d @file.json sends file contents. --data-raw treats the value literally — @ has no special meaning. For JSON bodies with characters that might be shell-expanded or misinterpreted, --data-raw or storing the body in a file with -d @body.json is safer than complex quoting.

How do I send JSON with curl?

Set Content-Type and provide the body: curl -X POST https://api.example.com/data -H "Content-Type: application/json" -d '{"key":"value"}'. On Windows, use double quotes with escaped inner quotes. Alternatively, write JSON to a file and use -d @body.json to avoid shell quoting issues. Pair with -H "Accept: application/json" if the server serves multiple formats.

What does curl -v output and how do I read it?

-v shows the full request/response conversation. > lines are request headers sent. < lines are response headers received. * lines are curl's informational messages (TLS handshake, connection events). The response body appears after the headers. For machine-readable output, use -w with %{http_code} instead of parsing -v with grep.

How do I follow redirects with curl?

Use -L (--location). curl does not follow redirects by default — a 301 just prints the Location header. -L follows up to 30 redirects. Add --max-redirs N to change the limit. Note: -L converts POST to GET on 301/302 by default. Use --post301 or --post302 to preserve the POST method through the redirect.

How do I test an API that requires a Bearer token?

Add the Authorization header: curl -H "Authorization: Bearer YOUR_TOKEN" https://api.example.com/resource. Store the token in an environment variable (export TOKEN=...) rather than pasting it directly into commands — it won't appear in shell history or process lists. Chain multiple -H flags for APIs requiring both auth and content type.

What is the curl -w flag and what can it measure?

-w prints formatted data after transfer. Key variables: %{http_code} (status), %{time_total} (total seconds), %{time_starttransfer} (TTFB), %{size_download} (bytes). Example: curl -s -o /dev/null -w "%{http_code} %{time_total}s" https://example.com prints "200 0.342s" — ideal for scripted health checks and latency monitoring in CI pipelines.

Format and Inspect Your API Responses

When curl returns JSON, pipe it through BytePane's JSON Formatter to validate structure and explore nested data. Need to encode URL parameters by hand? Use the URL Encoding Guide to understand percent-encoding, or look up what HTTP status codes mean in the HTTP Status Codes reference.

Open JSON Formatter

Related Articles