# Errors, rate limits & retries

> One error shape, a Retry-After you can trust, and a short list of what to retry.

- **HTML:** https://offendersearch.app/docs/platform/errors-and-retries
- **Base URL (live):** https://api.offendersearch.app — live keys (`os_live_…`)
- **Base URL (sandbox):** https://sandbox.offendersearch.app — test keys (`os_test_…`), never billed
- **Authentication:** `X-API-Key` request header
- **OpenAPI:** https://offendersearch.app/openapi.json · https://offendersearch.app/openapi.yaml
- **Docs index for agents:** https://offendersearch.app/llms.txt

## The error body

Two shapes, by product. **Sex Offender, Monitoring, Map, FCRA and every `401`/`402`:** JSON with a `detail`. For a field validation failure it is a list, one entry per field, each with `loc` (the path to the field), `msg` and `type`; for a refused request an object with a `code` and a sentence; on a `429` an object with a `code` and `retryAfterSeconds`. **Criminal Records refusals (`4xx`/`5xx` from the service):** JSON with an `error` object carrying `code` and `message`. Branch on the code, never on the sentence.

```json
// Sex Offender refusal
{ "detail": { "code": "query_too_broad",
              "message": "Add a first name, a date of birth or a state." } }

// Criminal Records refusal
{ "error": { "code": "query_too_broad",
             "message": "Add a first name, a date of birth or a state." } }
```

## Status codes

| Status | Meaning | Retry? |
| --- | --- | --- |
| `200` | The answer. Read `status` (`complete` / `partial`) before `records`. | — |
| `202` | Accepted: an async job with an id to poll (`POST /v1/searches`, `live.evidence`, a criminal search with more than two live sources). A Sex Offender live search that goes async answers `200` with `status: "pending"` and a `poll` path instead. | — |
| `400` / `422` | The request was refused: a missing required field, an invalid value, a query too broad to answer honestly. `detail` (criminal: `error.code`) says which. | No — fix the request. |
| `401` | Authentication failed. | No — fix the key or the host. |
| `403` | Not entitled (Map API, FCRA partner, consumer account on a business route). | No — enable it. |
| `404` | Unknown id or path. | No. |
| `429` | Rate limited or at capacity; nothing was searched or billed. `Retry-After` carries the seconds to wait (1–120); on the Sex Offender API `detail.retryAfterSeconds` repeats it. A criminal `429` comes from the gateway after it has queued the request for up to 45 s and may carry no JSON body — read the header. | Yes — after `Retry-After`. |
| `5xx` | The platform failed or a dependency timed out. Nothing was searched; nothing was billed. (`504` also answers a Sex Offender search that hit its `deadlineMs` with `onDeadline: "error"`, or that found nothing before some registries finished: `incomplete_no_results`.) | Yes — with backoff, for requests that are safe to repeat (below). |

## What to retry

- **Safe to retry:** every `GET`; a synchronous search without a `live` block; `429` after `Retry-After`.
- **Retry with care:** a synchronous search *with* a `live` block (a repeat re-checks the sources again and bills again) — read the error first.
- **Never retry blindly:** `POST /v1/searches` (async). Every accepted request is a new, billed job and there is no idempotency header. Persist the `searchId` from the `202` and recover by polling it. See [Async jobs & webhooks](/docs/platform/async-and-webhooks).

```python
import time, requests

def call(method, url, **kw):
    for attempt in range(4):
        r = requests.request(method, url, timeout=60, **kw)
        if r.status_code == 429:
            time.sleep(int(r.headers.get("Retry-After", "5")))
            continue
        if 500 <= r.status_code < 600 and method == "GET":
            time.sleep(2 ** attempt)
            continue
        return r
    return r
```

## Rate limits

Limits are per account and are enforced as `429` with `Retry-After`; there is no fixed public number, because the platform queues briefly before refusing and the headroom depends on your plan. A `429` is never a partial answer: nothing was searched and nothing was billed. Product pages note where a product queues or caps differently ([Sex Offender errors](/docs/errors), [Criminal errors](/docs/criminal/errors), [Map limits](/docs/map/limits-and-errors)).
