# Errors

> How the Fingerly API reports errors: one JSON error shape, conventional HTTP statuses, and stable error codes you can branch on.

Last updated: 2026-09-17

Errors use conventional HTTP statuses and one JSON body shape. Branch on `error.code`, which is stable; `error.message` is written for people and may change.

```json 422 Unprocessable Entity
{
  "error": {
    "code": "validation_error",
    "message": "The request could not be validated.",
    "status": 422,
    "details": [
      {
        "field": "tag",
        "location": "body",
        "issue": "must be a string"
      }
    ]
  },
  "request_id": "01a0a84c-0f11-7a3e-9c2d-4b5e6f708192"
}
```

**Error body**

- `error.code` (string): A stable, machine-readable code.
- `error.message` (string): A human-readable explanation.
- `error.status` (integer): The HTTP status, repeated.
- `error.details` (array): For validation errors: which `field`, where (`body`, `query`, `path`, `header`) and the `issue`. Omitted otherwise.
- `request_id` (string): The request's ID, also in the `X-Request-Id` header.

## Error codes

| Status | Code | What happened | What to do |
| --- | --- | --- | --- |
| `401` | `unauthorized` | The key did not authenticate for this request. | Check the key, its kind, its region and its allowed origins. |
| `402` | `billing_blocked` | The organization is not accepting traffic. | Resolve billing in the dashboard. |
| `402` | `no_credit` | The organization's balance is used up. | Add funds or turn on auto top-up. |
| `404` | `event_not_found` | No event with this request ID in the key's environment. | Check the ID and the environment. Events are readable for 30 days. |
| `409` | `supplement_conflict` | A different deferred report was already accepted for this request. | Do not retry with different content. |
| `413` | `payload_too_large` | The body is over 1 MiB. | Send a smaller body. |
| `415` | `unsupported_media_type` | The body is not JSON. | Send `Content-Type: application/json`. |
| `422` | `validation_error` | The body or parameters are malformed. | Read `error.details`. |
| `422` | `idempotency_key_too_long` | The `Idempotency-Key` is over 255 characters. | Use a shorter key, such as a UUID. |
| `422` | `invalid_request_id` | The request ID is not a Fingerly request ID. | Pass the `request_id` exactly as returned. |
| `422` | `invalid_window` | The time window, page, level or visitor filter is not valid. | See [List events](https://docs.fingerly.io/reference/list-events). |
| `422` | `invalid_supplement` | The deferred report or its token is not valid, or the token expired. | Deferred reports must arrive within 24 hours. |
| `422` | `invalid_visitor_metadata` | A proxy forwarded a malformed visitor address, origin or user agent. | Fix the proxy's forwarded headers. |
| `429` | `rate_limited` | The organization is over its rate limit. | Wait `Retry-After` seconds, then retry. |
| `503` | `service_unavailable` | Fingerly could not answer just now. | Retry with backoff and the same `Idempotency-Key`. |

## Retrying

- **Retry** `429` after `Retry-After`, and `5xx` and network errors with exponential backoff. Reuse the same `Idempotency-Key` so a retried identification is answered and charged once.
- **Do not retry** `401`, `402`, `413`, `415` or `422`. They describe the request or the account, not a passing condition.

> **Note:** The client SDKs already follow these rules. See [Idempotency and retries](https://docs.fingerly.io/reference/idempotency).
