Errors, status codes & rate limits
Every status code, the stable error envelope, the rate-limit headers, and how to back off.
Base URL https://api.offendersearch.appThis page as Markdown/docs/errors.mdStatus codes
The API uses conventional HTTP status codes. Note that a 200 can still carry a partial result: when you set a deadlineMs bound, the sources that completed within it are returned and every source is labelled in sourceStatus, rather than failing the whole call. Billing must be enabled on an account before the API returns live results.
| Status | Meaning | When you’ll see it |
|---|---|---|
| 200 | OK | Search completed — or returned partial results under the deadline (check status). |
| 202 | Accepted | Asynchronous search accepted; poll the results URL or await the webhook. |
| 400 | Bad Request | Invalid or missing parameters (e.g. no lastName and no q/location). |
| 401 | Unauthorized | Missing or invalid X-API-Key header, or a request outside the key’s scope. |
| 402 | Payment Required | Billing is not enabled on the account (required for all accounts, including verification reports). |
| 404 | Not Found | The search id or record id does not exist. |
| 422 | Unprocessable Entity | The request was understood and DECLINED, and the body says why in a plain sentence you can show a user. Two causes. (a) THE QUERY CANNOT BE ANSWERED COMPLETELY, so it is declined rather than answered with a subset that would read as the whole answer: a query carrying nothing to narrow on cannot be guaranteed to return the same full result twice. Send at least one of lastName, dob, age, city, zipcode, a lat/lng radius, or q — a firstName on its own is not enough to narrow a search, so pair it with a surname or a date of birth. A name + DOB query is NEVER refused, whatever the name. (b) A parameter is malformed or unrecognised — an unknown jurisdiction code, or a filter placed at the top level of the body instead of inside query. |
| 429 | Too Many Requests | Rate limit or plan quota exceeded. Back off and retry after the Retry-After header. |
| 504 | Deadline Exceeded | Returned only when the deadline is hit and onDeadline = "error". |
The error envelope
Every non-2xx response returns a JSON body with a stable error.code you can switch on and a human-readable error.message you can surface to a user. The shape is identical on every endpoint, so one error handler covers the whole API.
{
"error": {
"code": "invalid_request",
"message": "query requires at least one of lastName, q, or a lat/lng radius."
}
}{
"error": {
"code": "unprocessable_query",
"message": "A firstName on its own cannot be answered completely. Pair it with a lastName, a dob, or an age."
}
}422 is a deliberate contract, not a failure. A query with nothing to narrow on cannot be guaranteed to return the same full result twice, so it is declined with a sentence you can show a user rather than answered with a subset that would read as the whole answer. A name + date-of-birth query is never declined, whatever the name.Switching on the code
const res = await fetch(url, { method: "POST", headers, body });
if (!res.ok) {
const { error } = await res.json();
switch (res.status) {
case 401: throw new Error("Check X-API-Key: " + error.message);
case 402: throw new Error("Billing is not enabled on this account.");
case 422: return { declined: error.message }; // show the user
case 429: return retryAfter(res.headers.get("Retry-After"));
default: throw new Error(`${error.code}: ${error.message}`);
}
}
const body = await res.json();
// A 200 can still be partial — read status and counts, never records alone.
if (body.status === "partial") {
// counts.sourcesComplete of counts.sourcesQueried finished
}Rate limits & quotas
Requests are rate limited per API key. When you exceed your plan’s sustained rate or monthly quota the API responds with 429 Too Many Requests and a Retry-After header (seconds) telling you when to try again. Every response also carries the current limit state, so you can throttle proactively rather than discovering the ceiling on a rejection.
| Header | Meaning |
|---|---|
| X-RateLimit-Limit | Requests allowed in the current window. |
| X-RateLimit-Remaining | Requests left in the current window. |
| X-RateLimit-Reset | Unix time when the window resets. |
| Retry-After | On a 429, seconds to wait before retrying. |
Concurrency and monthly volume are set by your plan. For large jobs, prefer the asynchronous endpoint or batch and spread submissions, rather than issuing thousands of synchronous calls at once. Contact us to raise your limits. Handle 429 with exponential backoff that respects Retry-After.
import time, requests
def search(payload, key, attempts=5):
for attempt in range(attempts):
r = requests.post(
"https://api.offendersearch.app/v1/search",
headers={"X-API-Key": key},
json=payload,
timeout=130,
)
if r.status_code != 429:
r.raise_for_status()
return r.json()
wait = int(r.headers.get("Retry-After", 2 ** attempt))
time.sleep(wait)
raise RuntimeError("rate limited after %d attempts" % attempts)Retrying safely
A retry should never start — or bill — a second search. On POST /v1/searches send an Idempotency-Key header and a repeated key returns the original job rather than creating a new one, which makes a dropped-connection retry safe.
When a response comes back partial, the thing to read is not the status word but sourceStatus[].incompleteReason: it is a closed enum, and each value says whether retrying the identical request will change the answer. Result completeness sets out every value and the action it implies.