How to Debug API Issues: Tools and Techniques 2026
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 most expensive debugging mistake is jumping straight to code. Before modifying your application, reproduce the issue with curl. A single curl command tells you: is this a code problem, an auth problem, a network problem, or a provider problem? Answering that question takes 30 seconds with curl; it can take hours without it. This guide is organized around the reproducible curl-first approach.
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 |
Debugging Third-Party API Changes
A special category of API issues: the integration worked for months, then broke without any changes to your code. Third-party API changes are the culprit.
API versioning drift: Most APIs version via URL (/v1/, /v2/) or headers. If you're pinned to a version, breakage only happens when that version is sunset. If you're using unversioned endpoints or "latest", you're exposed to silent breaking changes. Always use explicit version numbers in your API calls.
Schema changes: Even within a version, providers add required fields, remove optional fields, change types (number string to actual number), or rename fields. Most providers give deprecation warnings with 6-12 months notice, but you need to watch their changelog. Subscribe to API changelogs via email, RSS, or the provider's status/developer newsletter. Some providers (Stripe, Twilio) have excellent changelog RSS feeds; others require checking a changelog page manually.
TLS and cipher changes: Less common but impactful: providers upgrade minimum TLS versions (dropping TLS 1.0/1.1) or change cipher suites. Older Node.js versions or corporate proxies with custom CA certificates can break when providers update their TLS configuration. If curl works but your app doesn't, check your Node.js version and TLS configuration.
Rate limit changes: Providers regularly adjust rate limits as they scale. What was unlimited yesterday might now be limited; what was 100 req/min might become 60 req/min. Monitor your 429 error rate in production — an increase in 429s without increased traffic volume usually indicates a rate limit change on the provider's end.
Debugging in Production
Development debugging is straightforward — you can log everything, add breakpoints, and iterate quickly. Production debugging requires different techniques.
Correlation IDs: Every outbound API call should include a unique request ID (either a standard header like X-Request-ID or the provider's own correlation ID). Store this ID in your logs alongside the request details. When a customer reports an issue at a specific time, you can search logs for the correlation ID and see exactly what was sent and what was received.
Structured logging for API calls: Log API calls as structured events, not strings. Include: timestamp, method, url (without sensitive query params), status code, latency, error message if applicable, and correlation ID. Structured logs enable querying ("show me all 401 errors in the last hour to api.stripe.com") rather than grep-based log archaeology.
Error sampling: High-volume APIs can generate thousands of errors per minute. Log every error, but sample detailed payloads. For 429 errors, log 1 in 100 with the full request details — enough to diagnose patterns without overwhelming your log storage. For 500 errors (rarer, more impactful), log every one with full detail.
Feature flag around new integrations: When adding a new API integration or switching providers, put it behind a feature flag. If something goes wrong in production, you can disable the new integration instantly without a deployment. This is especially important for payment and auth integrations where bugs have immediate user impact.
Environment-Specific Issues
A large category of API bugs only appear in specific environments — they work locally but fail in CI, or work in staging but fail in production. The root causes are almost always configuration drift or environment-specific network topology.
Environment variable leakage: The most common cause of "works locally, fails in CI" is a missing environment variable. Your local .env file has API_KEY=sk_test_abc but CI only has API_KEY defined in a secrets manager, and it wasn't provisioned. Add an explicit check at app startup that enumerates required environment variables and fails fast with a clear error: Error: Missing required env var: STRIPE_SECRET_KEY. This surfaces immediately instead of producing mysterious auth errors mid-request.
TLS certificate differences: Corporate VPNs and some CI environments intercept HTTPS traffic via a custom CA certificate. Your app may work fine on your laptop (where the CA cert is in the system trust store) but fail in a Docker container that doesn't have the custom CA. Symptoms: TLS handshake errors, UNABLE_TO_VERIFY_LEAF_SIGNATURE, or CERT_UNTRUSTED. Fix: pass the custom CA via NODE_EXTRA_CA_CERTS environment variable in Docker, rather than disabling certificate verification with NODE_TLS_REJECT_UNAUTHORIZED=0 (a serious security risk in production).
Proxy interference: In enterprise environments, outbound HTTP traffic routes through a proxy. Some proxies strip or modify headers, block certain IP ranges, or perform TLS inspection. If curl from your workstation works but the same request from an EC2 instance doesn't, the difference is usually the proxy configuration. Test with curl -v --proxy http://your-proxy:8080 https://api.example.com/v1/test to verify proxy behavior.
Clock skew: Some API providers sign requests with a timestamp and reject requests where the timestamp differs from the current time by more than a few minutes. AWS Signature Version 4, for example, rejects requests more than 5 minutes out of sync. EC2 instances generally have NTP configured, but Docker containers, VMs that have been suspended, or developer laptops with incorrect time zones can have skewed clocks. Check with date -u and compare against date -u on a known-good server.
Methodology
The curl timing format (%{time_namelookup}, %{time_connect}, etc.) is supported by curl 7.1+ and all modern macOS and Linux distributions. The request ID approach (crypto.randomUUID().slice(0, 8)) generates an 8-character hex prefix sufficient for correlation within a single request context; for distributed tracing across services, use a full UUID or adopt OpenTelemetry trace IDs. The checklist format at the end is derived from common API debugging runbooks at Stripe, Twilio, and Cloudflare developer documentation. Provider changelog subscription methods vary — Stripe has an RSS feed at stripe.com/docs/changelog and email notifications in the dashboard; Twilio has a status page and developer newsletter.
Find APIs with the best debugging tools on APIScout — request logs, sandbox environments, and error documentation ratings.
Related: Building an AI Agent in 2026, Building an AI-Powered App: Choosing Your API Stack, Building an API Marketplace