> ## Documentation Index
> Fetch the complete documentation index at: https://docs.metastreams.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors and retries

> The error envelope, every error code, and which errors to retry.

Every error the API raises has the same body:

```json theme={null}
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "One or more parameters are invalid.",
    "details": [
      { "field": "countBack", "message": "Must be between 1 and 5000." }
    ]
  }
}
```

| Field | Meaning |
| - | - |
| `code` | A stable, machine-readable code. Switch on this. |
| `message` | A sentence for the developer: what happened, and what to do next. Safe to log and display. Never parse it; the wording can change. |
| `details` | Present on some codes only. Its shape is fixed per code. |

The HTTP status always matches the code.

Two responses come from the HTTP layer instead, with no JSON body: a request with the wrong method for its path gets `405 Method Not Allowed`, and a request to `/v1/stream` without WebSocket upgrade headers gets a plain-text `4xx`.

## Error codes

| Code | Status | When | `details` | Retry? |
| - | - | - | - | - |
| `VALIDATION_ERROR` | 400 | A path parameter, query parameter or body is malformed | A list of `{field, message}` | No. Fix the request. |
| `UNSUPPORTED_CHAIN` | 400 | The chain slug is not [supported](/concepts/chains) | `{chain}` | No. Fix the request. |
| `UNAUTHORIZED` | 401 | The API key is missing, malformed, unknown, revoked or expired | — | No. Fix the key. See [Authentication](/get-started/authentication). |
| `CREDITS_EXHAUSTED` | 402 | Your credit balance cannot cover the request | `{required, remaining}` | No. Top up first. |
| `FORBIDDEN` | 403 | Your key is valid, but its plan does not include this endpoint | — | No. |
| `NOT_FOUND` | 404 | The path or resource does not exist | — | No. |
| `RATE_LIMITED` | 429 | You met a rate or connection limit | — | Yes, after `Retry-After`. |
| `INTERNAL_ERROR` | 500 | An unexpected failure on our side | — | Yes, with backoff, a few times. |
| `SERVICE_UNAVAILABLE` | 503 | A dependency is down, or maintenance is under way | — | Yes, with backoff. |

`CREDITS_EXHAUSTED` and `FORBIDDEN` are reserved. No key receives them today.

### Validation details

A `VALIDATION_ERROR` lists each problem by field, as in the example at the top of this page. `field` is the parameter's name as you sent it, or `path`, `query` or `body` when the problem is not tied to one field.

### Three causes of `429`

| Cause | What frees it |
| - | - |
| Your key spent its per-minute request budget | Time. Wait for `Retry-After`. |
| Your IP address sent too many requests | Time. Wait for `Retry-After`. |
| Your key holds the most open stream connections it may | Closing one of your connections. The message says so. |

See [Rate limits and headers](/concepts/rate-limits) for the numbers.

## Retry strategy

1. **Honour `Retry-After`.** Every `429` carries it, in whole seconds. Wait at least that long.
2. **Back off exponentially on `500` and `503`,** with jitter, so parallel workers do not retry in step.
3. **Cap your retries.** Three to five attempts are enough. Past that, the problem needs attention.
4. **Never retry a `4xx` other than `429`.** It fails the same way every time.

```python theme={null}
import random
import time

import requests

RETRYABLE = {429, 500, 503}

def get_with_retries(url, headers, attempts=5):
    for attempt in range(attempts):
        response = requests.get(url, headers=headers, timeout=10)
        if response.status_code not in RETRYABLE or attempt == attempts - 1:
            return response
        retry_after = response.headers.get("Retry-After")
        delay = int(retry_after) if retry_after else min(30, 2 ** attempt) + random.random()
        time.sleep(delay)
```

## Request IDs

Every response carries an `X-Request-ID` header. You can also send your own `X-Request-ID`: up to 128 visible ASCII characters, with no spaces. The API echoes it back. It replaces any other value with a generated ID rather than rejecting the request.

When you contact support about a failed request, include its `X-Request-ID`.

## Errors on streams

A refused stream frame gets an `error` frame with the same `code`, `message` and `details`. See [Connecting to streams](/streams/overview#errors).

## New codes and fields

The API evolves without breaking your integration:

* **New error codes can appear.** If you meet a code you do not recognize, handle the response by its HTTP status.
* **New response fields can appear.** Ignore fields you do not recognize.
* **Codes are never renamed, repurposed or removed** within a version.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.