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:
- Validate input before sending requests
- Use schema validation libraries (Joi, Yup, Pydantic)
- Read error response body for specific field errors
- Log malformed requests for debugging
- 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-Afterheader - 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:
- Visit API Status Check
- Check specific services:
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:
- Check overall API status
- Monitor specific services you depend on
- Subscribe to alerts for instant notifications
- View historical uptime and incident patterns
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 →