# Errors

Errors use standard HTTP status codes and a consistent JSON body.

## Error format

400 Bad Request

```json
{
 "error": {
 "code": "invalid_parameter",
 "message": "limit must be between 1 and 100",
 "status": 400
 }
}
```

Branch on `error.code`, which is stable. `error.message` is for humans and may change.

## Error codes

| Status | Code | What it means |
| --- | --- | --- |
| 400 | `invalid_parameter` | A query parameter is missing, malformed or out of range. The message names the parameter. |
| 401 | `missing_api_key` | No key was sent. Use the `Authorization: Bearer` header, `X-API-Key`, or `?api_key=`. |
| 401 | `invalid_api_key` | The key doesn't exist or was revoked. Create a new one in the dashboard. |
| 403 | `plan_required` | The feature isn't in your plan (for example, the WebSocket stream on Free). |
| 404 | `not_found` | The article or route doesn't exist. |
| 429 | `daily_limit_reached` | Free plan only: you used 100 requests today (UTC). Resets at 00:00 UTC, or upgrade for unlimited. |
| 429 | `rate_limited` | More than 20 requests in one second from the same account. Back off for the `Retry-After` seconds. |
| 500 | `server_error` | Something broke on our side. Retry with backoff; we're alerted automatically. |
| 503 | `unavailable` | A dependency is temporarily down. Retry with backoff and check the status page. |

## What to retry

- **Retry with backoff:** `429 rate_limited` (honor `Retry-After`), `500`, `502`, `503`, `504` and network errors. Use exponential backoff with jitter, starting around 1 second.
- **Don't retry:** other `4xx` errors. Fix the request, key or plan first.
- **Free plan:** after `429 daily_limit_reached`, wait until 00:00 UTC or upgrade.

retry helper

```python
import random
import time

import requests

def get_with_retry(url, headers, params, attempts=5):
 for i in range(attempts):
 r = requests.get(url, headers=headers, params=params, timeout=10)
 if r.status_code == 429 or r.status_code >= 500:
 wait = float(r.headers.get("Retry-After", 2 ** i)) + random.random()
 time.sleep(wait)
 continue
 r.raise_for_status()
 return r.json()
 r.raise_for_status()
```
