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
| Tool | What It Does | When to Use |
|---|---|---|
| curl | Raw HTTP requests | Quick API testing, reproducing issues |
| Postman / Insomnia | GUI API client | Complex requests, environment management |
| httpie | Human-friendly curl | Readable CLI testing |
| Browser DevTools | Network tab inspection | Client-side API debugging |
| Wireshark | Packet-level inspection | Network issues, TLS problems |
| ngrok / localtunnel | Expose localhost | Webhook debugging |
| mitmproxy | HTTP proxy | Intercept and modify requests |
| jq | JSON processor | Parse 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:
- Wrong key (test key in prod, or vice versa)
- Expired key or token
- Wrong header format
- Key has insufficient permissions/scopes
- 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:
- DNS resolution slow or failing
- Server is overloaded
- Your request is too complex (large payload, expensive query)
- Network issues between you and the API
- 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:
- Missing required fields
- Wrong data types (string instead of number)
- Invalid format (email, date, URL)
- Exceeding field limits (max length, max items)
- 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:
- Endpoint URL is wrong or unreachable
- Endpoint returns non-2xx status
- Signature verification failing (wrong secret, body parsing issue)
- Firewall blocking the webhook sender's IP
- 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:
- API version changed (you're hitting v2, expecting v1 format)
- Content negotiation (getting XML instead of JSON)
- Error response has different format than success response
- HTML error page instead of JSON (CDN or proxy intercepting)
- 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
| Mistake | Impact | Fix |
|---|---|---|
| Not reading error response body | Missing the diagnostic info | Always log full error response |
| Debugging in code before trying curl | Harder to isolate | Reproduce with curl first |
| Not checking API status page | Wasting time on provider issue | Check status.provider.com first |
| Swallowing errors silently | Problems go undetected | Log every non-2xx response |
| No request ID tracking | Can't correlate logs | Add unique request ID to every call |
| Not checking API changelog | Miss breaking changes | Subscribe to API changelog |
Find APIs with the best debugging tools on APIScout — request logs, sandbox environments, and error documentation ratings.