Skip to main content

How to Debug API Issues: Tools and Techniques

·APIScout Team
debuggingapi integrationtoolstroubleshootingdeveloper experience

How to Debug API Issues: Tools and Techniques

Something is broken. The API call that worked yesterday returns errors today. Your webhook handler isn't firing. The response is missing fields. Debugging API issues requires the right tools and a systematic approach — not guessing.

The Debugging Toolkit

Essential Tools

ToolWhat It DoesWhen to Use
curlRaw HTTP requestsQuick API testing, reproducing issues
Postman / InsomniaGUI API clientComplex requests, environment management
httpieHuman-friendly curlReadable CLI testing
Browser DevToolsNetwork tab inspectionClient-side API debugging
WiresharkPacket-level inspectionNetwork issues, TLS problems
ngrok / localtunnelExpose localhostWebhook debugging
mitmproxyHTTP proxyIntercept and modify requests
jqJSON processorParse and filter API responses

Quick Debugging Commands

# Basic API test
curl -v https://api.example.com/health

# Verbose output (see headers, TLS, timing)
curl -v -H "Authorization: Bearer $API_KEY" \
  https://api.example.com/v1/users

# Time the request
curl -o /dev/null -s -w "DNS: %{time_namelookup}s\nConnect: %{time_connect}s\nTLS: %{time_appconnect}s\nFirst byte: %{time_starttransfer}s\nTotal: %{time_total}s\n" \
  https://api.example.com/v1/users

# Pretty-print JSON response
curl -s https://api.example.com/v1/users | jq .

# Compare expected vs actual response
diff <(curl -s api.example.com/v1/users | jq .) expected.json

# Test with httpie (more readable)
http GET api.example.com/v1/users Authorization:"Bearer $API_KEY"

Common Issues and Fixes

Issue 1: 401 Unauthorized

# Diagnose: Is the key correct?
curl -v -H "Authorization: Bearer $API_KEY" https://api.example.com/v1/me

# Check: Is the key in the right format?
echo "Key starts with: ${API_KEY:0:7}"  # Should be sk_live_, pk_test_, etc.

# Check: Is the header format correct?
# ❌ Wrong
curl -H "Authorization: $API_KEY" ...
curl -H "Authorization: Token $API_KEY" ...
curl -H "Api-Key: $API_KEY" ...

# ✅ Correct (depends on provider)
curl -H "Authorization: Bearer $API_KEY" ...     # OAuth/JWT
curl -H "X-Api-Key: $API_KEY" ...                # Some REST APIs
curl -u "$API_KEY:" ...                           # Basic auth (Stripe)

Common causes:

  1. Wrong key (test key in prod, or vice versa)
  2. Expired key or token
  3. Wrong header format
  4. Key has insufficient permissions/scopes
  5. Environment variable not loaded

Issue 2: Timeout / No Response

# Diagnose: Where is the delay?
curl -o /dev/null -s -w "DNS: %{time_namelookup}s\nTCP: %{time_connect}s\nTLS: %{time_appconnect}s\nWait: %{time_starttransfer}s\nTotal: %{time_total}s\n" \
  https://api.example.com/v1/slow-endpoint

# Results:
# DNS: 0.050s     ← Slow? DNS issue
# TCP: 0.150s     ← Slow? Network/routing issue
# TLS: 0.300s     ← Slow? TLS handshake issue
# Wait: 5.000s    ← Slow? Server processing issue
# Total: 5.100s

Common causes:

  1. DNS resolution slow or failing
  2. Server is overloaded
  3. Your request is too complex (large payload, expensive query)
  4. Network issues between you and the API
  5. Missing timeout in your code (hanging indefinitely)

Issue 3: 400 Bad Request

# Diagnose: What exactly is wrong?
curl -s -X POST https://api.example.com/v1/users \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $API_KEY" \
  -d '{"email": "bad-format", "name": ""}' | jq .

# Good APIs tell you exactly what's wrong:
# {
#   "error": {
#     "type": "validation_error",
#     "errors": [
#       { "field": "email", "message": "Invalid email format" },
#       { "field": "name", "message": "Name is required" }
#     ]
#   }
# }

Common causes:

  1. Missing required fields
  2. Wrong data types (string instead of number)
  3. Invalid format (email, date, URL)
  4. Exceeding field limits (max length, max items)
  5. Wrong Content-Type header

Issue 4: Webhook Not Firing

# Step 1: Verify your endpoint is accessible
curl -X POST https://your-app.com/webhooks/stripe \
  -H "Content-Type: application/json" \
  -d '{"test": true}'

# Step 2: Check webhook logs in provider dashboard
# Stripe: Dashboard → Developers → Webhooks → select endpoint → Recent deliveries
# Most providers show request/response for each webhook attempt

# Step 3: For local development, use ngrok
ngrok http 3000
# Use the ngrok URL as your webhook endpoint

# Step 4: Verify signature handling
# Common mistake: reading body as JSON then verifying raw body

Common causes:

  1. Endpoint URL is wrong or unreachable
  2. Endpoint returns non-2xx status
  3. Signature verification failing (wrong secret, body parsing issue)
  4. Firewall blocking the webhook sender's IP
  5. Webhook events not enabled for the event type you need

Issue 5: Unexpected Response Format

// Diagnose: Log the actual response before parsing
async function debugApiCall(url: string) {
  const response = await fetch(url, {
    headers: { 'Authorization': `Bearer ${API_KEY}` },
  });

  // Log raw response info
  console.log('Status:', response.status);
  console.log('Headers:', Object.fromEntries(response.headers));

  const text = await response.text();
  console.log('Raw body:', text);

  // Then try to parse
  try {
    return JSON.parse(text);
  } catch {
    console.error('Response is not JSON:', text.substring(0, 200));
    throw new Error(`Expected JSON, got: ${text.substring(0, 100)}`);
  }
}

Common causes:

  1. API version changed (you're hitting v2, expecting v1 format)
  2. Content negotiation (getting XML instead of JSON)
  3. Error response has different format than success response
  4. HTML error page instead of JSON (CDN or proxy intercepting)
  5. Empty response body

Debugging Patterns

Request/Response Logging

// Add logging middleware to catch everything
function createLoggingFetch() {
  return async function loggingFetch(url: string, options?: RequestInit) {
    const requestId = crypto.randomUUID().slice(0, 8);
    const start = Date.now();

    console.log(`[${requestId}] → ${options?.method || 'GET'} ${url}`);

    if (options?.body) {
      console.log(`[${requestId}] Body:`, typeof options.body === 'string'
        ? options.body.substring(0, 500)
        : options.body
      );
    }

    try {
      const response = await fetch(url, options);
      const duration = Date.now() - start;

      console.log(`[${requestId}] ← ${response.status} (${duration}ms)`);

      // Clone response to read body without consuming it
      const clone = response.clone();
      const body = await clone.text();

      if (!response.ok) {
        console.error(`[${requestId}] Error body:`, body.substring(0, 1000));
      }

      return response;
    } catch (error) {
      console.error(`[${requestId}] ✗ Network error after ${Date.now() - start}ms:`, error);
      throw error;
    }
  };
}

Diff Responses Over Time

// Save API responses and compare when things break
import { writeFile, readFile } from 'fs/promises';

async function captureBaseline(name: string, url: string) {
  const response = await fetch(url);
  const data = await response.json();
  await writeFile(
    `debug/baselines/${name}.json`,
    JSON.stringify(data, null, 2)
  );
}

async function compareWithBaseline(name: string, currentData: any) {
  const baseline = JSON.parse(
    await readFile(`debug/baselines/${name}.json`, 'utf-8')
  );

  const baselineKeys = Object.keys(flatten(baseline));
  const currentKeys = Object.keys(flatten(currentData));

  const added = currentKeys.filter(k => !baselineKeys.includes(k));
  const removed = baselineKeys.filter(k => !currentKeys.includes(k));

  if (added.length || removed.length) {
    console.warn('API response changed!');
    if (added.length) console.warn('New fields:', added);
    if (removed.length) console.warn('Missing fields:', removed);
  }
}

Debugging Checklist

When an API call fails:

1. [ ] Can you reproduce with curl? (Isolate from your code)
2. [ ] Is the API key correct and not expired?
3. [ ] Is the request format correct? (Content-Type, body format)
4. [ ] What does the error response say? (Read the full body)
5. [ ] Is the API status page showing issues?
6. [ ] Has the API version changed?
7. [ ] Are you hitting rate limits?
8. [ ] Is there a network issue? (DNS, firewall, proxy)
9. [ ] Does it work with a different API key/account?
10. [ ] Check the provider's changelog for recent changes

Common Mistakes

MistakeImpactFix
Not reading error response bodyMissing the diagnostic infoAlways log full error response
Debugging in code before trying curlHarder to isolateReproduce with curl first
Not checking API status pageWasting time on provider issueCheck status.provider.com first
Swallowing errors silentlyProblems go undetectedLog every non-2xx response
No request ID trackingCan't correlate logsAdd unique request ID to every call
Not checking API changelogMiss breaking changesSubscribe to API changelog

Find APIs with the best debugging tools on APIScout — request logs, sandbox environments, and error documentation ratings.

Comments