← Back to blog
curl-builder

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.

curlrest-apitestingdebuggingdeveloper-tools

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 jq


jq 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/octocat


Output (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-8


Key things to look for in verbose output:

  • TLS version — some APIs reject TLS 1.2 connections now.
  • Request headers actually sent — curl adds defaults like User-Agent that your code might not.
  • Response status code — the real one, not what your HTTP library reports after following redirects.
  • Rate limit headersx-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/health


    Output: HTTP 200 | Time: 0.143s

    If 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/me


    Step 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.txt


    2>&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 1


    Save 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-endpoint


    Output:

    DNS Lookup:    0.004s
    TCP Connect:   0.024s
    TLS Handshake: 0.062s
    First Byte:    1.847s
    Total:         1.852s


    The 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.