API Error Codes Explained: Complete Guide to HTTP Status Codes (2026)

API Error Codes Explained: Complete Guide to HTTP Status Codes (2026)

Quick Answer: API error codes are standardized HTTP status codes (100-599) that indicate the result of an API request. The most critical ones developers encounter are: 400 (Bad Request) for malformed requests, 401 (Unauthorized) for authentication failures, 404 (Not Found) for missing resources, 429 (Too Many Requests) for rate limiting, 500 (Internal Server Error) for server crashes, 502 (Bad Gateway) for proxy failures, and 503 (Service Unavailable) for temporary outages.

Understanding API error codes is fundamental to building resilient applications. Whether you're integrating with Stripe payments, GitHub APIs, or any other service, knowing how to interpret and handle these status codes can save hours of debugging and prevent costly production failures.

What Are HTTP Status Codes?

HTTP status codes are three-digit numbers returned by servers in response to client requests. They're defined by the Internet Engineering Task Force (IETF) in RFC 7231 and subsequent specifications. Every API response includes a status code in the HTTP header, indicating whether the request succeeded, failed, or requires additional action.

The Five Status Code Categories

Status codes are organized into five classes, each identified by the first digit:

  • 1xx (Informational): Request received, continuing process
  • 2xx (Success): Request successfully received, understood, and accepted
  • 3xx (Redirection): Further action needed to complete the request
  • 4xx (Client Error): Request contains bad syntax or cannot be fulfilled
  • 5xx (Server Error): Server failed to fulfill a valid request

This hierarchical structure allows developers to implement general error handling strategies while also addressing specific error conditions.

Complete HTTP Status Code Reference

1xx Informational Responses

Code Status Meaning
100 Continue Initial part of request received; client should continue
101 Switching Protocols Server is switching protocols as requested by client
102 Processing Server received and is processing the request (WebDAV)
103 Early Hints Return some response headers before final HTTP message

Usage in APIs: Rarely seen in modern REST APIs. Primarily used in HTTP/1.1 protocol negotiations and WebDAV extensions.

2xx Success Responses

Code Status Meaning
200 OK Standard success response; request fulfilled
201 Created Resource successfully created (POST/PUT requests)
202 Accepted Request accepted for processing but not completed
203 Non-Authoritative Information Returned data may be from a cached or modified source
204 No Content Request successful but no content to return
205 Reset Content Request successful; client should reset document view
206 Partial Content Server returning only part of resource (range requests)
207 Multi-Status Multiple status codes for multiple operations (WebDAV)
208 Already Reported Members already enumerated (WebDAV)
226 IM Used Instance manipulations applied successfully

Most common in APIs: 200 (GET requests), 201 (resource creation), 204 (deletion success).

3xx Redirection Responses

Code Status Meaning
300 Multiple Choices Multiple options for the resource
301 Moved Permanently Resource permanently moved to new URL
302 Found Resource temporarily at different URL
303 See Other Response found at different URL using GET
304 Not Modified Cached version is still valid
305 Use Proxy Resource must be accessed through specified proxy
307 Temporary Redirect Same as 302 but method must not change
308 Permanent Redirect Same as 301 but method must not change

Most common in APIs: 301 (API versioning migrations), 302/307 (temporary OAuth redirects), 304 (conditional requests).

4xx Client Error Responses

Code Status Meaning
400 Bad Request Malformed request syntax or invalid parameters
401 Unauthorized Authentication required or failed
402 Payment Required Reserved for future use (payment systems)
403 Forbidden Server understood but refuses to authorize
404 Not Found Requested resource does not exist
405 Method Not Allowed HTTP method not supported for this endpoint
406 Not Acceptable Server cannot produce response matching accept headers
407 Proxy Authentication Required Client must authenticate with proxy
408 Request Timeout Server timed out waiting for request
409 Conflict Request conflicts with current server state
410 Gone Resource permanently deleted
411 Length Required Content-Length header required
412 Precondition Failed Preconditions in headers not met
413 Payload Too Large Request entity exceeds server limits
414 URI Too Long Request URI exceeds server limits
415 Unsupported Media Type Request media type not supported
416 Range Not Satisfiable Range header cannot be fulfilled
417 Expectation Failed Expect header requirements cannot be met
418 I'm a teapot April Fools' joke (RFC 2324)
421 Misdirected Request Request directed at wrong server
422 Unprocessable Entity Syntax valid but semantically incorrect
423 Locked Resource is locked (WebDAV)
424 Failed Dependency Request failed due to previous failure (WebDAV)
425 Too Early Server unwilling to risk processing early data
426 Upgrade Required Client must upgrade to different protocol
428 Precondition Required Origin server requires conditional request
429 Too Many Requests Rate limit exceeded
431 Request Header Fields Too Large Header fields too large
451 Unavailable For Legal Reasons Resource blocked for legal reasons

Most critical for APIs: 400, 401, 403, 404, 409, 422, 429.

5xx Server Error Responses

Code Status Meaning
500 Internal Server Error Generic server error
501 Not Implemented Server doesn't support requested functionality
502 Bad Gateway Gateway/proxy received invalid response from upstream
503 Service Unavailable Server temporarily unavailable (maintenance/overload)
504 Gateway Timeout Gateway/proxy timeout waiting for upstream
505 HTTP Version Not Supported HTTP version in request not supported
506 Variant Also Negotiates Server configuration error
507 Insufficient Storage Server cannot store representation (WebDAV)
508 Loop Detected Infinite loop detected (WebDAV)
510 Not Extended Further extensions required
511 Network Authentication Required Client must authenticate to gain network access

Most critical for APIs: 500, 502, 503, 504.

Most Common API Errors Developers Face

400 Bad Request: Malformed or Invalid Input

What it means: Your request syntax is incorrect, missing required parameters, or contains invalid data types.

Common causes:

  • Missing required fields in request body
  • Invalid JSON formatting
  • Incorrect data types (string instead of integer)
  • Invalid parameter values
  • Malformed query strings

Real-world example:

// ❌ Bad - Missing required field
fetch('https://api.example.com/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    name: 'John Doe'
    // Missing required 'email' field
  })
});
// Response: 400 Bad Request
// { "error": "Missing required field: email" }

// ✅ Good - All required fields included
fetch('https://api.example.com/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    name: 'John Doe',
    email: 'john@example.com'
  })
});

Python example:

import requests

# ❌ Bad - Invalid data type
response = requests.post('https://api.example.com/products', 
  json={
    'name': 'Widget',
    'price': '19.99'  # Should be float, not string
  }
)
# Response: 400 Bad Request

# ✅ Good - Correct data types
response = requests.post('https://api.example.com/products',
  json={
    'name': 'Widget',
    'price': 19.99
  }
)

How to handle it:

  1. Validate input before sending requests
  2. Use schema validation libraries (Joi, Yup, Pydantic)
  3. Read error response body for specific field errors
  4. Log malformed requests for debugging
  5. Never retry 400 errors without fixing the request

401 Unauthorized vs 403 Forbidden: Authentication vs Authorization

These two codes are commonly confused but have distinct meanings:

401 Unauthorized: "Who are you?"

Authentication failed or credentials not provided. The request requires user authentication.

Common causes:

  • Missing API key or access token
  • Expired authentication token
  • Invalid credentials
  • Malformed Authorization header

Example:

// ❌ Missing authentication
fetch('https://api.example.com/private-data')
  .then(res => res.json());
// Response: 401 Unauthorized

// ✅ With authentication
fetch('https://api.example.com/private-data', {
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
  }
})
.then(res => res.json());

403 Forbidden: "I know who you are, but you can't do that."

Authentication succeeded, but the authenticated user lacks permission for the requested resource or action.

Common causes:

  • Insufficient user permissions/roles
  • API key lacks required scopes
  • Resource access restricted by business rules
  • IP address not allowlisted
  • Account suspended or rate limited

Example:

import requests

headers = {'Authorization': 'Bearer USER_TOKEN'}

# ✅ User authenticated successfully
# ❌ But lacks admin permission
response = requests.delete(
  'https://api.example.com/admin/users/123',
  headers=headers
)
# Response: 403 Forbidden
# { "error": "Admin privileges required" }

Key distinction:

  • 401: Fix authentication (add/refresh token)
  • 403: Fix authorization (upgrade permissions, verify scopes)

Retrying a 403 without changing permissions will always fail.

404 Not Found: Missing Resources

What it means: The requested resource doesn't exist at the specified endpoint.

Common causes:

  • Incorrect URL or endpoint path
  • Resource ID doesn't exist
  • Resource was deleted
  • Typo in request path
  • API version mismatch

Example:

// ❌ Resource doesn't exist
fetch('https://api.example.com/users/99999')
  .then(res => {
    if (res.status === 404) {
      console.log('User not found');
    }
  });

// Proper error handling
async function getUser(userId) {
  const response = await fetch(`https://api.example.com/users/${userId}`);
  
  if (response.status === 404) {
    throw new Error(`User ${userId} not found`);
  }
  
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }
  
  return response.json();
}

Python handling:

import requests
from requests.exceptions import HTTPError

def get_user(user_id):
    response = requests.get(f'https://api.example.com/users/{user_id}')
    
    try:
        response.raise_for_status()
        return response.json()
    except HTTPError as e:
        if e.response.status_code == 404:
            print(f"User {user_id} not found")
            return None
        raise

Best practices:

  • Verify endpoint URLs in API documentation
  • Check if resource exists before attempting operations
  • Implement graceful degradation for missing resources
  • Don't retry 404s—the resource won't suddenly appear

429 Too Many Requests: Rate Limiting

What it means: You've exceeded the API's rate limit. This is the API's way of preventing abuse and ensuring fair resource distribution.

Common causes:

  • Too many requests in a short time period
  • Burst traffic exceeding limits
  • Missing rate limit implementation in client
  • Concurrent requests exceeding quota
  • Shared API key across multiple services

Example with retry logic:

async function fetchWithRetry(url, options = {}, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    const response = await fetch(url, options);
    
    if (response.status === 429) {
      // Check for Retry-After header
      const retryAfter = response.headers.get('Retry-After');
      const waitTime = retryAfter 
        ? parseInt(retryAfter) * 1000 
        : Math.pow(2, i) * 1000; // Exponential backoff
      
      console.log(`Rate limited. Waiting ${waitTime}ms before retry ${i + 1}`);
      await new Promise(resolve => setTimeout(resolve, waitTime));
      continue;
    }
    
    return response;
  }
  
  throw new Error('Max retries exceeded');
}

// Usage
fetchWithRetry('https://api.example.com/data')
  .then(res => res.json())
  .then(data => console.log(data))
  .catch(err => console.error('Failed after retries:', err));

Python with exponential backoff:

import requests
import time
from typing import Optional

def fetch_with_retry(url: str, max_retries: int = 3) -> Optional[dict]:
    for attempt in range(max_retries):
        response = requests.get(url)
        
        if response.status_code == 429:
            retry_after = response.headers.get('Retry-After')
            wait_time = int(retry_after) if retry_after else 2 ** attempt
            
            print(f"Rate limited. Waiting {wait_time}s before retry {attempt + 1}")
            time.sleep(wait_time)
            continue
        
        response.raise_for_status()
        return response.json()
    
    raise Exception('Max retries exceeded')

Rate limit best practices:

  • Always check Retry-After header
  • Implement exponential backoff
  • Cache responses when possible
  • Use webhooks instead of polling
  • Monitor your rate limit usage
  • Consider upgrading API tier if consistently hitting limits

500 Internal Server Error: Generic Server Failure

What it means: Something went wrong on the server side, but the server can't be more specific about what happened.

Common causes:

  • Unhandled exceptions in server code
  • Database connection failures
  • Memory exhaustion
  • Configuration errors
  • Null pointer exceptions
  • Dependency service failures

Handling strategy:

async function robustApiCall(url, options = {}) {
  const maxRetries = 3;
  
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);
      
      // 500 errors might be transient—retry with backoff
      if (response.status === 500) {
        if (attempt < maxRetries - 1) {
          const backoff = Math.pow(2, attempt) * 1000;
          console.warn(`Server error (500). Retrying in ${backoff}ms...`);
          await new Promise(resolve => setTimeout(resolve, backoff));
          continue;
        }
        throw new Error('Server error persisted after retries');
      }
      
      if (!response.ok) {
        throw new Error(`HTTP error ${response.status}`);
      }
      
      return await response.json();
      
    } catch (error) {
      if (attempt === maxRetries - 1) throw error;
      await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, attempt)));
    }
  }
}

// Usage with fallback
async function getUserDataSafely(userId) {
  try {
    return await robustApiCall(`https://api.example.com/users/${userId}`);
  } catch (error) {
    console.error('Failed to fetch user data:', error);
    // Return cached data or default
    return getCachedUserData(userId) || { id: userId, name: 'Unknown' };
  }
}

When to retry 500 errors:

  • Do retry: Transient server issues often resolve quickly
  • Use backoff: Exponential backoff prevents overwhelming struggling servers
  • Limit retries: 3-5 attempts maximum
  • Don't retry infinitely: If server is truly down, retries won't help
  • Monitor patterns: Persistent 500s indicate serious API issues

Check if it's a widespread outage: Visit API Status Check to verify if other users are experiencing issues with services like Stripe, Twilio, or SendGrid.

502 Bad Gateway: Proxy or Gateway Failure

What it means: A server acting as a gateway or proxy received an invalid response from an upstream server.

Common scenarios:

  • Load balancer cannot reach backend servers
  • API gateway timeout communicating with microservices
  • Reverse proxy configuration error
  • Upstream server crashed or is unresponsive
  • Network connectivity issues between proxy and origin

Architecture where 502 occurs:

Client → CDN → Load Balancer → API Gateway → Microservice
                     ↑
                   502 here if microservice unreachable

Handling approach:

import requests
import time
from typing import Optional

def call_api_with_502_handling(url: str, timeout: int = 10) -> Optional[dict]:
    """
    502 errors often indicate temporary gateway issues.
    Retry a few times with increasing delays.
    """
    retries = [1, 2, 4, 8]  # Backoff delays in seconds
    
    for i, delay in enumerate(retries):
        try:
            response = requests.get(url, timeout=timeout)
            
            if response.status_code == 502:
                if i < len(retries) - 1:
                    print(f"502 Bad Gateway. Retrying in {delay}s...")
                    time.sleep(delay)
                    continue
                else:
                    print("API gateway issues persist. Service may be down.")
                    # Check status page
                    print("Check https://apistatuscheck.com for service status")
                    return None
            
            response.raise_for_status()
            return response.json()
            
        except requests.exceptions.Timeout:
            print(f"Request timeout after {timeout}s")
            if i < len(retries) - 1:
                time.sleep(delay)
                continue
            return None
    
    return None

502 vs 500:

  • 500: Application server error (bug in code)
  • 502: Infrastructure error (gateway can't reach app)

When seeing 502s, check if the entire service is down using API Status Check. Services like Vercel, Heroku, and Render occasionally experience gateway issues.

503 Service Unavailable: Temporary Outage

What it means: The server is temporarily unable to handle requests, usually due to maintenance or overload.

Common causes:

  • Scheduled maintenance
  • Server overload (too much traffic)
  • Database connection pool exhausted
  • Deployment in progress
  • Deliberate service disable (circuit breaker activated)

Key difference from other 5xx errors: 503 explicitly signals the issue is temporary. The service should recover.

Proper handling with Retry-After:

async function fetchWith503Handling(url, options = {}) {
  const response = await fetch(url, options);
  
  if (response.status === 503) {
    const retryAfter = response.headers.get('Retry-After');
    
    if (retryAfter) {
      // Retry-After can be seconds or HTTP date
      const isDate = isNaN(retryAfter);
      const waitSeconds = isDate 
        ? (new Date(retryAfter) - new Date()) / 1000
        : parseInt(retryAfter);
      
      console.log(`Service unavailable. Retry after ${waitSeconds}s`);
      
      // Queue for retry or notify user
      return {
        retry: true,
        retryAfter: waitSeconds,
        message: 'Service temporarily unavailable'
      };
    }
  }
  
  return response;
}

// Graceful degradation example
async function getDataWithFallback(url) {
  try {
    const response = await fetchWith503Handling(url);
    
    if (response.retry) {
      // Show user-friendly message
      return {
        error: true,
        message: `Service is temporarily down. Please try again in ${response.retryAfter} seconds.`,
        retryAfter: response.retryAfter
      };
    }
    
    return await response.json();
    
  } catch (error) {
    // Check if widespread outage
    console.log('Check service status at https://apistatuscheck.com');
    throw error;
  }
}

Best practices for 503:

  • Respect Retry-After header
  • Show user-friendly message ("Service temporarily unavailable")
  • Implement graceful degradation (cached data, reduced functionality)
  • Subscribe to status updates for critical services
  • Don't retry aggressively (respect server's recovery time)

Check real-time status at API Status Check for services like AWS, Cloudflare, and Supabase.

Complete Error Handling Strategy

JavaScript/TypeScript API Client

class ApiClient {
  constructor(baseUrl, apiKey) {
    this.baseUrl = baseUrl;
    this.apiKey = apiKey;
    this.maxRetries = 3;
  }
  
  async request(endpoint, options = {}) {
    const url = `${this.baseUrl}${endpoint}`;
    const config = {
      ...options,
      headers: {
        'Authorization': `Bearer ${this.apiKey}`,
        'Content-Type': 'application/json',
        ...options.headers
      }
    };
    
    for (let attempt = 0; attempt < this.maxRetries; attempt++) {
      try {
        const response = await fetch(url, config);
        
        // Handle different status codes
        switch (response.status) {
          case 200:
          case 201:
          case 204:
            return response.status === 204 ? null : await response.json();
          
          case 400:
            const errorData = await response.json();
            throw new ValidationError(errorData.message, errorData.fields);
          
          case 401:
            // Try to refresh token
            await this.refreshAuth();
            if (attempt < this.maxRetries - 1) continue;
            throw new AuthenticationError('Authentication failed');
          
          case 403:
            throw new AuthorizationError('Insufficient permissions');
          
          case 404:
            throw new NotFoundError(`Resource not found: ${endpoint}`);
          
          case 429:
            const retryAfter = response.headers.get('Retry-After');
            const waitTime = retryAfter ? parseInt(retryAfter) * 1000 : 2000 * Math.pow(2, attempt);
            console.log(`Rate limited. Waiting ${waitTime}ms`);
            await this.sleep(waitTime);
            continue;
          
          case 500:
          case 502:
          case 503:
          case 504:
            if (attempt < this.maxRetries - 1) {
              const backoff = 1000 * Math.pow(2, attempt);
              console.log(`Server error ${response.status}. Retrying in ${backoff}ms`);
              await this.sleep(backoff);
              continue;
            }
            throw new ServerError(`Server error: ${response.status}`);
          
          default:
            throw new ApiError(`Unexpected status: ${response.status}`);
        }
        
      } catch (error) {
        if (error instanceof ApiError) throw error;
        
        // Network errors
        if (attempt === this.maxRetries - 1) {
          throw new NetworkError('Network request failed', error);
        }
        
        await this.sleep(1000 * Math.pow(2, attempt));
      }
    }
  }
  
  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
  
  async refreshAuth() {
    // Implement token refresh logic
  }
}

// Custom error classes
class ApiError extends Error {
  constructor(message) {
    super(message);
    this.name = this.constructor.name;
  }
}

class ValidationError extends ApiError {
  constructor(message, fields) {
    super(message);
    this.fields = fields;
  }
}

class AuthenticationError extends ApiError {}
class AuthorizationError extends ApiError {}
class NotFoundError extends ApiError {}
class ServerError extends ApiError {}
class NetworkError extends ApiError {}

// Usage
const api = new ApiClient('https://api.example.com', 'your-api-key');

try {
  const user = await api.request('/users/123');
  console.log(user);
} catch (error) {
  if (error instanceof ValidationError) {
    console.error('Validation failed:', error.fields);
  } else if (error instanceof NotFoundError) {
    console.error('User not found');
  } else if (error instanceof ServerError) {
    console.error('Server error. Check https://apistatuscheck.com');
  }
}

Python API Client with Comprehensive Error Handling

import requests
import time
from typing import Optional, Dict, Any
from requests.exceptions import RequestException, Timeout, ConnectionError

class ApiClient:
    def __init__(self, base_url: str, api_key: str, max_retries: int = 3):
        self.base_url = base_url
        self.api_key = api_key
        self.max_retries = max_retries
        self.session = requests.Session()
        self.session.headers.update({
            'Authorization': f'Bearer {api_key}',
            'Content-Type': 'application/json'
        })
    
    def request(self, method: str, endpoint: str, **kwargs) -> Optional[Dict[Any, Any]]:
        url = f"{self.base_url}{endpoint}"
        
        for attempt in range(self.max_retries):
            try:
                response = self.session.request(method, url, **kwargs)
                
                # Handle successful responses
                if response.status_code in (200, 201):
                    return response.json()
                
                if response.status_code == 204:
                    return None
                
                # Handle client errors (4xx)
                if response.status_code == 400:
                    error_data = response.json()
                    raise ValidationError(error_data.get('message'), error_data.get('fields'))
                
                if response.status_code == 401:
                    # Try to refresh authentication
                    if attempt < self.max_retries - 1:
                        self._refresh_auth()
                        continue
                    raise AuthenticationError('Authentication failed')
                
                if response.status_code == 403:
                    raise AuthorizationError('Insufficient permissions')
                
                if response.status_code == 404:
                    raise NotFoundError(f'Resource not found: {endpoint}')
                
                if response.status_code == 409:
                    raise ConflictError('Resource conflict')
                
                if response.status_code == 422:
                    error_data = response.json()
                    raise ValidationError('Unprocessable entity', error_data)
                
                if response.status_code == 429:
                    retry_after = int(response.headers.get('Retry-After', 2 ** attempt))
                    print(f"Rate limited. Waiting {retry_after}s")
                    time.sleep(retry_after)
                    continue
                
                # Handle server errors (5xx)
                if response.status_code in (500, 502, 503, 504):
                    if attempt < self.max_retries - 1:
                        backoff = 2 ** attempt
                        print(f"Server error {response.status_code}. Retrying in {backoff}s")
                        time.sleep(backoff)
                        continue
                    raise ServerError(f'Server error: {response.status_code}')
                
                # Unexpected status codes
                raise ApiError(f'Unexpected status: {response.status_code}')
            
            except (ConnectionError, Timeout) as e:
                if attempt == self.max_retries - 1:
                    raise NetworkError(f'Network error: {str(e)}')
                time.sleep(2 ** attempt)
        
        raise ApiError('Max retries exceeded')
    
    def _refresh_auth(self):
        # Implement token refresh logic
        pass
    
    def get(self, endpoint: str, **kwargs):
        return self.request('GET', endpoint, **kwargs)
    
    def post(self, endpoint: str, **kwargs):
        return self.request('POST', endpoint, **kwargs)
    
    def put(self, endpoint: str, **kwargs):
        return self.request('PUT', endpoint, **kwargs)
    
    def delete(self, endpoint: str, **kwargs):
        return self.request('DELETE', endpoint, **kwargs)

# Custom exceptions
class ApiError(Exception):
    pass

class ValidationError(ApiError):
    def __init__(self, message, fields=None):
        super().__init__(message)
        self.fields = fields

class AuthenticationError(ApiError):
    pass

class AuthorizationError(ApiError):
    pass

class NotFoundError(ApiError):
    pass

class ConflictError(ApiError):
    pass

class ServerError(ApiError):
    pass

class NetworkError(ApiError):
    pass

# Usage
if __name__ == '__main__':
    api = ApiClient('https://api.example.com', 'your-api-key')
    
    try:
        user = api.get('/users/123')
        print(f"User: {user}")
    except ValidationError as e:
        print(f"Validation error: {e.fields}")
    except NotFoundError:
        print("User not found")
    except ServerError:
        print("Server error. Check https://apistatuscheck.com for status")
    except NetworkError as e:
        print(f"Network error: {e}")

Debugging API Errors: Step-by-Step Strategy

1. Capture Complete Error Context

Always log:

  • HTTP status code
  • Response headers (especially Retry-After, X-RateLimit-*)
  • Response body
  • Request method and URL
  • Request headers (sanitize sensitive data)
  • Timestamp
  • User context (if applicable)
function logApiError(error, request, response) {
  console.error({
    timestamp: new Date().toISOString(),
    request: {
      method: request.method,
      url: request.url,
      headers: sanitizeHeaders(request.headers)
    },
    response: {
      status: response.status,
      statusText: response.statusText,
      headers: Object.fromEntries(response.headers.entries()),
      body: response.body
    },
    error: error.message,
    stack: error.stack
  });
}

2. Use cURL to Reproduce

Test the API directly with cURL to isolate client-side issues:

# Basic GET request
curl -v https://api.example.com/users/123 \
  -H "Authorization: Bearer YOUR_TOKEN"

# POST with JSON body
curl -v -X POST https://api.example.com/users \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"John","email":"john@example.com"}'

# Check response headers
curl -I https://api.example.com/users/123

# Follow redirects
curl -L https://api.example.com/resource

3. Check API Documentation

Verify:

  • Endpoint URL is correct
  • HTTP method is supported
  • Required headers are present
  • Request body schema matches documentation
  • Authentication format is correct
  • API version is current

4. Inspect Network Traffic

Use browser DevTools or proxy tools:

  • Chrome DevTools: Network tab → Filter by API domain
  • Postman: Built-in console shows raw requests/responses
  • Charles Proxy: Intercept and modify API calls
  • Wireshark: Deep packet inspection for network issues

5. Test with API Clients

Use dedicated tools to isolate code issues:

  • Postman: Visual API testing, collections, environments
  • Insomnia: Lightweight alternative to Postman
  • HTTPie: Command-line HTTP client with clean syntax
  • Bruno: Open-source API client

6. Check Service Status

Before deep debugging, verify the API isn't experiencing an outage:

When It's You vs When It's the API

Signs It's Your Code

Consistent errors with same pattern

  • Always 400 on same endpoint
  • Always 401 with specific credentials
  • Always 422 with specific payload

Works in API testing tools but not your code

  • Postman succeeds, your app fails
  • cURL works, SDK doesn't

Error message points to your request

  • "Missing required field: email"
  • "Invalid parameter: date_format"
  • "Unsupported HTTP method"

Only affects your application

  • No reports from other developers
  • Service status shows operational

Signs It's the API

🚨 Intermittent errors with no pattern

  • Same request sometimes succeeds, sometimes fails
  • Random timeouts

🚨 Elevated 5xx error rates

  • Sudden spike in 500/502/503 responses
  • Errors across multiple endpoints

🚨 Other developers reporting issues

  • Status page shows incident
  • Community forums have recent complaints
  • Social media mentions of outages

🚨 Response times degraded

  • Usually 200ms, now 5+ seconds
  • Frequent timeouts

Quick Decision Tree

Is the error 5xx (500, 502, 503, 504)?
├─ Yes → Check API Status Check for service outage
│   ├─ Service shows downtime → It's the API, implement retry/fallback
│   └─ Service shows operational → Check your network/proxy
│
└─ Is the error 4xx (400, 401, 403, 404, 429)?
    ├─ 401/403 → Check authentication/authorization
    ├─ 429 → Implement rate limiting
    └─ 400/422 → Validate your request payload

Use API Status Check to quickly determine if it's a widespread issue:

Don't waste hours debugging your code when the API is down. Verify service status first!

Frequently Asked Questions

What's the difference between 401 Unauthorized and 403 Forbidden?

401 Unauthorized means authentication failed or wasn't provided—the server doesn't know who you are. Fix by providing valid credentials, refreshing expired tokens, or checking API key validity. 403 Forbidden means authentication succeeded but you lack permission—the server knows who you are but you can't access this resource. Fix by upgrading permissions, requesting additional scopes, or verifying account status.

Should I retry 4xx errors?

Generally no, with one exception: 429 (Too Many Requests) should be retried with exponential backoff and respect for the Retry-After header. Other 4xx errors indicate client-side problems (bad request, auth failure, not found) that won't resolve without changing your request. Retrying 400, 401, 403, 404 without fixing the underlying issue wastes resources and may trigger rate limiting.

What's the best way to handle rate limiting (429)?

Always check the Retry-After response header first—it tells you exactly when to retry. If absent, implement exponential backoff starting at 1-2 seconds. Limit total retries to 3-5 attempts. Better yet, proactively prevent 429s by: tracking your request rate, implementing client-side rate limiting, caching responses, using webhooks instead of polling, and monitoring X-RateLimit-* headers to stay within limits.

When should I implement exponential backoff?

Use exponential backoff for transient errors that might resolve with time: 429 (rate limiting), 500 (internal server error), 502 (bad gateway), 503 (service unavailable), and 504 (gateway timeout). Don't use it for permanent errors like 400 (bad request), 404 (not found), or 410 (gone)—these won't fix themselves. Start with 1 second delay, double on each retry, cap at 30-60 seconds, and limit total attempts to 3-5.

How can I tell if an API is down or if it's my code?

First, check API Status Check to see if others are reporting issues. If the service shows operational, test with cURL or Postman to isolate your code. Signs it's the API: intermittent 5xx errors, elevated error rates across endpoints, degraded response times, other developers reporting issues. Signs it's your code: consistent 4xx errors, works in API clients but not your app, error messages point to your request format.

What does 502 Bad Gateway actually mean?

502 Bad Gateway means a proxy or gateway (load balancer, API gateway, reverse proxy) received an invalid response from an upstream server. Common causes: backend service crashed, microservice unreachable, network partition between gateway and origin, timeout from slow upstream service. It's an infrastructure problem, not your code. Usually transient—retry with backoff. If persistent, check API Status Check for service outages.

Should I log API errors with sensitive information?

Never log sensitive data: API keys, passwords, tokens, authorization headers, PII (email, phone, address), payment details, or session IDs. Do log: sanitized headers (remove Authorization), request/response bodies without sensitive fields, status codes, timestamps, endpoint URLs, error messages, retry attempts, and user IDs (hashed). Implement log scrubbing to automatically redact sensitive patterns before writing to logs.

What's the difference between 500, 502, and 503?

500 Internal Server Error: Generic application error—bug in server code, unhandled exception, database query failure. The application itself crashed. 502 Bad Gateway: Infrastructure error—gateway/proxy can't reach the application server. Network or deployment issue. 503 Service Unavailable: Temporary unavailability—server is alive but deliberately rejecting requests (maintenance, overload, circuit breaker). Often includes Retry-After header. All indicate server-side problems, but 503 explicitly signals it's temporary.

How do I handle API versioning and deprecation warnings?

Check for deprecation headers like X-API-Deprecation-Date, Sunset, or Warning. Monitor API changelog and subscribe to developer newsletters. When a version is deprecated: Immediate: log deprecation warnings prominently. Short-term: test your integration against new API version in staging. Medium-term: update client code to use new endpoints/schemas. Before sunset: complete migration and remove old version dependencies. Maintain backward compatibility during transition period.

What's the best way to monitor API health in production?

Implement multi-layered monitoring: Application-level: log all API errors with context, track error rates by status code, monitor response time percentiles. Infrastructure-level: use API Status Check for external monitoring, set up alerts for elevated error rates, track uptime and availability metrics. Business-level: monitor conversion rates (failed checkouts indicate API issues), track revenue impact of API errors. Set up alerting channels (PagerDuty, Slack, email) for critical services.

Stay Ahead of API Issues

Don't let API errors catch you off guard. Whether you're integrating with Stripe, Twilio, SendGrid, or any critical service, knowing their real-time status can save hours of debugging.

API Status Check monitors 100+ services 24/7:

  • ⚡ Real-time health checks every 60 seconds
  • 📊 Response time and latency tracking
  • 🔔 Instant alerts via email, Slack, Discord, or webhook
  • 📈 Historical uptime data and incident reports
  • 🌍 Multi-region monitoring

Start monitoring your APIs now →


Last updated: February 4, 2026. This guide covers HTTP status codes as defined in RFC 7231 and commonly used by modern REST APIs.

Monitor Your APIs

Check the real-time status of 100+ popular APIs used by developers.

View API Status →