How to Test REST APIs with Curl
Step-by-step tutorial on debugging REST APIs using curl, jq, and verbose mode. Includes Stripe, GitHub, and custom API examples with real commands.
How to Test REST API Curl
Your API returns a 500 and the logs say nothing useful. The frontend team says the request "worked in Postman." You need to reproduce the exact HTTP call, inspect every header, and see the raw response — fast. curl gives you that control. This guide walks through a complete API testing workflow: from basic GET calls to authenticated mutations, piped through jq for readable output, with verbose mode to catch what you would otherwise miss. Every example runs against real public APIs so you can follow along.
Setting Up Your Terminal for API Testing
Before diving into requests, make sure your environment has the essentials.
# Check curl version (7.68+ recommended for --retry-all-errors)
curl --version
Install jq if missing
macOS
brew install jq
Ubuntu/Debian
sudo apt-get install jq
Windows (via scoop)
scoop install jqjq is not optional for serious API testing. Raw JSON responses from production APIs are often minified — hundreds of fields on a single line. jq makes them readable and filterable.
The Verbose Flag: Your Primary Debugging Tool
When an API call fails, the response body rarely tells the full story. Verbose mode shows the complete HTTP conversation.
curl -v https://api.github.com/users/octocatOutput (abbreviated):
* Trying 140.82.121.6:443...
* Connected to api.github.com (140.82.121.6) port 443
* ALPN: offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256
> GET /users/octocat HTTP/2
> Host: api.github.com
> User-Agent: curl/8.4.0
> Accept: */*
>
< HTTP/2 200
< x-ratelimit-limit: 60
< x-ratelimit-remaining: 58
< content-type: application/json; charset=utf-8Key things to look for in verbose output:
User-Agent that your code might not.x-ratelimit-remaining tells you if throttling is about to hit.Testing Against the GitHub API
GitHub's REST API is well-documented and has generous unauthenticated rate limits (60 req/hour). It is ideal for practice.
Fetch a user profile
curl -s https://api.github.com/users/octocat | jq '{login, name, public_repos, followers}'Output:
{
"login": "octocat",
"name": "The Octocat",
"public_repos": 8,
"followers": 12345
}List repositories with sorting
curl -s "https://api.github.com/users/octocat/repos?sort=updated&per_page=3" \
| jq '.[].full_name'Output:
"octocat/Hello-World"
"octocat/Spoon-Knife"
"octocat/octocat.github.io"Create an issue (authenticated)
curl -X POST https://api.github.com/repos/YOUR_USER/YOUR_REPO/issues \
-H "Authorization: Bearer ghp_your_token_here" \
-H "Content-Type: application/json" \
-d '{
"title": "Bug: API returns 500 on empty payload",
"body": "Steps to reproduce:\n1. Send POST with empty body\n2. Observe 500 response",
"labels": ["bug"]
}' | jq '{number, html_url, state}'The
| jq filter at the end extracts only the fields you care about from the full response.Testing Against the Stripe API
Stripe's API uses Basic auth with your secret key as the username and an empty password. This is a common pattern.
Retrieve your account
curl -s https://api.stripe.com/v1/account \
-u sk_test_your_key_here: \
| jq '{id, business_profile: .business_profile.name, country}'Note the trailing colon after the key — it tells curl the password is empty.
Create a customer
curl -X POST https://api.stripe.com/v1/customers \
-u sk_test_your_key_here: \
-d "email=test@luxkern.com" \
-d "name=Test Customer" \
-d "metadata[source]=curl-test" \
| jq '{id, email, created}'Stripe uses form-encoded bodies, not JSON. Each
-d flag adds a field. Nested objects use bracket notation (metadata[source]).List recent charges with filters
curl -s "https://api.stripe.com/v1/charges?limit=5&created[gte]=1719792000" \
-u sk_test_your_key_here: \
| jq '.data[] | {id, amount, currency, status}'The
created[gte] filter is a Unix timestamp. curl handles the brackets in the URL correctly — no escaping needed in most shells.Testing Your Own Custom API
Real-world debugging means testing your own endpoints. Here is a systematic approach.
Step 1: Verify the endpoint is reachable
curl -s -o /dev/null -w "HTTP %{http_code} | Time: %{time_total}s\n" \
https://api.yourapp.com/healthOutput:
HTTP 200 | Time: 0.143sIf this fails, you have a networking or deployment issue — stop debugging the application logic.
Step 2: Test authentication
# Should return 401
curl -s -o /dev/null -w "%{http_code}" https://api.yourapp.com/me
Should return 200
curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $API_TOKEN" \
https://api.yourapp.com/meStep 3: Test the failing endpoint with verbose mode
curl -v -X POST https://api.yourapp.com/orders \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"product_id": "prod_abc123",
"quantity": 2,
"shipping_address": {
"line1": "123 Dev Street",
"city": "San Francisco",
"state": "CA",
"zip": "94102"
}
}' 2>&1 | tee /tmp/api-debug.txt2>&1 merges stderr (verbose output) with stdout (response body). tee writes everything to a file while still printing to the terminal. You now have a complete record of the failed request.Step 4: Inspect the response body
curl -s -X POST https://api.yourapp.com/orders \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d @order-payload.json \
| jq .Storing the payload in a file (
order-payload.json) makes it easy to tweak and re-run without editing the command.Advanced jq Patterns for API Testing
jq is a full programming language for JSON. Here are patterns specifically useful when debugging APIs.
Extract error messages from nested responses
curl -s https://api.yourapp.com/orders \
-H "Authorization: Bearer $EXPIRED_TOKEN" \
| jq '{status: .status, error: .error.message, code: .error.code}'Count items in a paginated list
curl -s "https://api.yourapp.com/products?per_page=100" \
-H "Authorization: Bearer $API_TOKEN" \
| jq '.data | length'Filter array elements
curl -s "https://api.yourapp.com/orders?status=all" \
-H "Authorization: Bearer $API_TOKEN" \
| jq '[.data[] | select(.status == "failed")] | length'This tells you how many failed orders exist without writing a single line of application code.
Transform response into CSV
curl -s "https://api.yourapp.com/users?per_page=100" \
-H "Authorization: Bearer $API_TOKEN" \
| jq -r '.data[] | [.id, .email, .created_at] | @csv'Output:
"usr_1","alice@example.com","2026-01-15T10:30:00Z"
"usr_2","bob@example.com","2026-02-20T14:45:00Z"Pipe to a file and open in a spreadsheet — instant report.
Automating API Test Suites with Curl
For repeated testing, wrap curl calls in a shell script with assertions.
#!/usr/bin/env bash
set -euo pipefail
BASE_URL="https://api.yourapp.com"
TOKEN="$API_TOKEN"
PASS=0
FAIL=0
assert_status() {
local description="$1"
local expected="$2"
local actual="$3"
if [ "$actual" = "$expected" ]; then
echo "PASS: $description (HTTP $actual)"
((PASS++))
else
echo "FAIL: $description (expected $expected, got $actual)"
((FAIL++))
fi
}
Test 1: Health check
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/health")
assert_status "Health check" "200" "$STATUS"
Test 2: Auth required
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/me")
assert_status "Unauthenticated returns 401" "401" "$STATUS"
Test 3: Auth works
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $TOKEN" "$BASE_URL/me")
assert_status "Authenticated returns 200" "200" "$STATUS"
Test 4: Create resource
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST "$BASE_URL/items" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "test-item"}')
assert_status "Create item returns 201" "201" "$STATUS"
Test 5: Invalid payload
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST "$BASE_URL/items" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"invalid_field": true}')
assert_status "Invalid payload returns 422" "422" "$STATUS"
echo ""
echo "Results: $PASS passed, $FAIL failed"
[ "$FAIL" -eq 0 ] || exit 1Save as
api-tests.sh, run in CI, and you have a lightweight smoke test suite that catches regressions without any test framework.Timing Breakdown: Finding Slow Endpoints
When an API feels slow, measure where the time goes.
curl -s -o /dev/null -w "\
DNS Lookup: %{time_namelookup}s\n\
TCP Connect: %{time_connect}s\n\
TLS Handshake: %{time_appconnect}s\n\
First Byte: %{time_starttransfer}s\n\
Total: %{time_total}s\n" \
https://api.yourapp.com/expensive-endpointOutput:
DNS Lookup: 0.004s
TCP Connect: 0.024s
TLS Handshake: 0.062s
First Byte: 1.847s
Total: 1.852sThe gap between TLS Handshake and First Byte is server processing time. In this example, the server took 1.785 seconds to generate the response — that is your bottleneck.
For continuous monitoring of these metrics, set up endpoint checks with Luxkern PingCheck to get alerted when response times degrade.
Try PingCheck free — no credit card required.
Testing Webhooks with Curl and WebhookTunnel
When your API sends webhooks, you need to verify the payload reaches the consumer. Use curl to simulate the webhook sender and WebhookTunnel to expose your local consumer.
# Start your local webhook handler
node server.js # listening on port 3000
In another terminal, start the tunnel
webhooktunnel --port 3000
Simulate a webhook delivery
curl -X POST https://your-id.webhooktunnel.luxkern.com/webhooks \
-H "Content-Type: application/json" \
-H "X-Webhook-Signature: sha256=abc123..." \
-d '{
"event": "payment.completed",
"data": {
"id": "pay_123",
"amount": 4999,
"currency": "usd"
}
}'Your local server receives the exact HTTP request as if it came from a production webhook sender. Check our curl command examples reference for more payload formats.
Common Mistakes When Testing APIs with Curl
Forgetting Content-Type on POST
# Wrong — server may reject or misparse the body
curl -X POST https://api.example.com/data -d '{"key": "value"}'
Correct
curl -X POST https://api.example.com/data \
-H "Content-Type: application/json" \
-d '{"key": "value"}'Not following redirects
Many APIs redirect from
http:// to https:// or from a vanity domain to the actual API host. Without -L, curl returns the redirect response (usually empty).Ignoring rate limits
Check
x-ratelimit-remaining in the response headers before looping curl in a script. Burn through your quota and you will get 429 Too Many Requests for the next hour.Using -X GET explicitly
# Unnecessary — GET is the default
curl -X GET https://api.example.com/data
Cleaner
curl https://api.example.com/data-X GET is not harmful, but it adds noise. Worse, some developers copy it and change GET to POST without adding -d, which sends a POST with an empty body — often not what the server expects.Monitoring Your APIs After Testing
Testing proves an API works right now. Monitoring proves it keeps working. Once your curl-based tests pass, set up automated checks.
The metrics curl gives you — status code, response time, TLS validity — are exactly what monitoring tools track. Luxkern PingCheck runs these checks from multiple global locations on a schedule and alerts you via Slack, email, or webhook when something degrades.
For a deeper dive into monitoring strategy, read how to monitor API endpoints.
Summary
curl plus jq is a complete API testing toolkit that fits in your terminal. Start with verbose mode to understand what is actually happening on the wire. Use jq to extract, filter, and transform responses. Wrap repeated tests in shell scripts for CI. Measure timing breakdowns to find bottlenecks.
For a quick reference of every curl flag mentioned here, see our curl command examples cheat sheet.
Try Luxkern's developer tools free — no credit card required.