---
title: Errors
description: The Qaf API's JSON error format, every status and code it returns, which errors to retry, and how request IDs help support find a request.
---

An error response has a 4xx or 5xx status and a JSON body with the same shape on every endpoint:

```json
{
  "code": "NOT_FOUND",
  "status": 404,
  "message": "No passage has ID d9335f69-e54b-59ae-a78c-50db734b8d51."
}
```

| Field | Description |
| --- | --- |
| `code` | A stable, machine-readable code. Branch on this, not on `message`. |
| `status` | The HTTP status, repeated. |
| `message` | A sentence for people. Its wording can change. |
| `data` | Details for some codes, described below. Left out when there are none. |

## Status codes

| Status | `code` | When | Retry |
| --- | --- | --- | --- |
| `400` | `BAD_REQUEST` | The body isn't valid JSON, or the request can't be run as sent, such as a filter with an unknown author ID. | No. Fix the request. |
| `401` | `UNAUTHORIZED` | The API key is missing, invalid or revoked. The `WWW-Authenticate` header is `Bearer realm="api.qaf.ai"`, with `error="invalid_token"` added when the request sent a key. | No. |
| `403` | `FORBIDDEN` | The key's account can't use the API. | No. |
| `404` | `NOT_FOUND` | No passage has the ID, or no endpoint matches the method and path. | No. |
| `405` | `METHOD_NOT_SUPPORTED` | The path exists but takes another method. The `Allow` header lists the methods it takes. | No. |
| `413` | `PAYLOAD_TOO_LARGE` | The request body is larger than 64 KiB. | No. |
| `422` | `INPUT_VALIDATION_FAILED` | A field is missing, has the wrong type or breaks a limit, or the search body has a field it doesn't define. | No. Fix the request. |
| `429` | `RATE_LIMITED` | The account went over a [rate limit](/api/rate-limits), or your address sent too many keys that don't exist (`invalid_keys`). | Only when `data.reason` is `minute`, after `Retry-After` seconds. |
| `500` | `INTERNAL_SERVER_ERROR` | Something went wrong on our side. | Yes, with backoff. |
| `503` | `SERVICE_UNAVAILABLE` | Qaf couldn't finish the request within 30 seconds, or a service it relies on is down. | Yes, with backoff. |

Handle codes you don't know by their status: new codes can appear in v1.

## Validation errors

A `422` lists the problems in `data`. `fieldErrors` is keyed by the field's name, and `formErrors` holds problems that belong to no single field:

```json
{
  "code": "INPUT_VALIDATION_FAILED",
  "status": 422,
  "message": "✖ Too big: expected number to be <=15\n  → at limit",
  "data": {
    "formErrors": [],
    "fieldErrors": {
      "limit": ["Too big: expected number to be <=15"]
    }
  }
}
```

A field the search body doesn't define appears in `fieldErrors` under the top-level field that holds it: `topK` under `topK`, and an unknown filter key under `filters`. `message` gives the full path, so you can tell which field to drop:

```json
{
  "code": "INPUT_VALIDATION_FAILED",
  "status": 422,
  "message": "✖ Unrecognized key: \"categoryIds\"\n  → at filters.categoryIds",
  "data": {
    "formErrors": [],
    "fieldErrors": {
      "filters": ["Unrecognized key: \"categoryIds\""]
    }
  }
}
```

When the unknown key is the only one in `filters`, `filters` also lists `Specify at least one filter, or omit filters for an unrestricted query.`

## Requests that can't be run

A `400` from `POST /v1/search` carries `data.reason`:

| `reason` | Means |
| --- | --- |
| `unknown_author_id` | An ID in `filters.authorIds` isn't in the catalog. Look the author up with [`GET /v1/catalog/lookup`](/reference/catalog/lookup-catalog). |
| `unknown_book_id` | An ID in `filters.bookIds` isn't in the catalog. |
| `no_matching_books` | No book can match the filters together, such as a genre and a death-year range none of its authors fall in. |

New reasons can appear in v1. When `reason` is missing or unknown, show `message`.

## Rate limit errors

A `429` has a `Retry-After` header and `data`:

```json
{
  "code": "RATE_LIMITED",
  "status": 429,
  "message": "Too many requests. Try again later.",
  "data": { "retryAfter": 23, "reason": "minute" }
}
```

`retryAfter` is the same number of seconds as the header. `reason` says which limit was reached. Only a `minute` refusal is worth waiting out: a `day` refusal can last most of a day, and `invalid_keys` means the key you sent doesn't exist or was revoked. See [Back off and retry](/api/rate-limits#back-off-and-retry).

## Request IDs

Every API response, including errors, has an `X-Request-Id` header. The only exception is the answer to a browser's CORS preflight (`OPTIONS`) request. Log the ID with failed requests, and include it when you email us at contact@qaf.ai, so we can find the request.
