# Criminal Search API sandbox & test keys

> Test the Criminal Search API in the sandbox with an os_test_ key: fixed test names for search, batch, records, exclusions, reports and sources. Never billed.

- **HTML:** https://offendersearch.app/docs/criminal/sandbox
- **Base URL (sandbox):** https://sandbox.offendersearch.app — test keys (`os_test_…`), never billed; see https://offendersearch.app/docs/criminal/sandbox.md
- **Base URL (live):** https://api.offendersearch.app — live keys (`os_live_…`)
- **Authentication:** `X-API-Key` request header
- **OpenAPI:** https://offendersearch.app/openapi.json · https://offendersearch.app/openapi.yaml
- **Criminal reference as markdown:** https://offendersearch.app/docs/criminal.md

## Criminal search API test keys: create one in the dashboard

The criminal search sandbox is the Criminal Search API on a separate server: search, batch, stored searches, records, reports, jurisdictions, sources and resolve, with the same paths and response shapes as live. It answers from a fixed set of made-up people, so every request returns the same result every time, and nothing you send is billed.

1. In the dashboard, open [Developers → API keys](https://offendersearch.app/dashboard/keys).
2. Choose **Create test key**. Test keys are for business accounts and need no subscription.
3. Copy the key. Test keys begin `os_test_`. Like a live key, the full secret is shown once, so put it in your secrets store straight away.

One test key works on every API’s sandbox. Each key in the list is labelled **Test · Sandbox** or **Live**, with the base URL it works on. Send it in the same `X-API-Key` header the live API uses. An account holds up to 3 active test keys and can create 10 a day. A revoked test key stops working on the sandbox within a few minutes.

## Criminal search sandbox base URL vs live

|  | Sandbox | Production |
| --- | --- | --- |
| Base URL | `https://sandbox.offendersearch.app` | `https://api.offendersearch.app` |
| Key prefix | `os_test_` | `os_live_` |
| Data | The fixed test people listed below | The criminal records dataset |
| Billing | Never billed, never recorded to usage | Your plan’s prices |
| `sandbox` field | `true` on every response | Absent |
| `X-Offendersearch-Environment` | `sandbox` on every response | Absent |

Each key works on its own server only, so a mixed-up configuration is caught on the first request rather than after a release. A test key sent to the live API, or a live key sent to the sandbox, is refused with `401` and a `detail` message that says so:

401 — a test key sent to the live API

```json
{
  "detail": "This is a test key (os_test_…). Test keys work only on the sandbox at https://sandbox.offendersearch.app, which answers with sample data. To search real records, send a live key (os_live_…) to https://api.offendersearch.app."
}
```

401 — a live key sent to the sandbox

```json
{
  "detail": "This is a live key. The sandbox at https://sandbox.offendersearch.app accepts test keys (os_test_…) only — create one in the dashboard under Developers → API keys. Send live keys to https://api.offendersearch.app."
}
```

A revoked test key gets `401` with: “This test key has been revoked. Create a new test key in the dashboard under Developers → API keys.”

Each test key can send 60 requests a minute and 2,000 a day, and one IP address 120 a minute, across every API on the sandbox. Past a limit the sandbox answers `429` with a `Retry-After` header. See [Sandbox rate limits](https://offendersearch.app/docs/sandbox.md#rate-limits).

## Criminal search test names

Send these to `POST /v1/criminal/search` on the sandbox. They work the same way on `POST /v1/criminal/batch`, and searches and records open with `GET /v1/criminal/searches/{searchId}` and `GET /v1/criminal/records/{recordId}`. Read `recordType` before describing a record: an arrest is not a conviction, and a warrant means the person is sought. Field definitions are on the [criminal Record object](https://offendersearch.app/docs/criminal/record-object.md).

| Send | What comes back |
| --- | --- |
| `firstName: Clear`, `lastName: Record` | **Clear record.** No match: a complete answer with zero records. |
| `firstName: Felony`, `lastName: Record`, `dob: 1975-06-15` | **Felony conviction.** One court case: a felony burglary conviction with case number, court, charges, plea, disposition and sentence. DOB-verified. |
| `firstName: Felony`, `lastName: Record` | **Felony conviction, no date of birth sent.** The same court case, matched on name alone: matchState no_dob_age_year and unverified true, because no date of birth was sent to check. |
| `firstName: Felony`, `lastName: Record`, `dob: 1990-01-01` | **Felony record name, different date of birth.** No match: the record's date of birth contradicts the one sent. |
| `firstName: Misdemeanor`, `lastName: Record`, `dob: 1988-11-02` | **Misdemeanor conviction.** One court case: a misdemeanor theft conviction with a fine. |
| `firstName: Arrest`, `lastName: Only`, `dob: 1992-04-20` | **Arrest only.** One jail booking with a charge at booking and no disposition. A booking is not a conviction. |
| `firstName: Active`, `lastName: Warrant`, `dob: 1985-09-09` | **Active warrant.** One open warrant (currentStatus active_warrant). |
| `firstName: In`, `lastName: Custody`, `dob: 1979-12-31` | **Currently in custody.** One state prison record: currentlyIncarcerated true, custodyFacility, projected release date. |
| `firstName: Possible`, `lastName: Match` | **Possible matches, name only.** Three records with the same name in three states and no date of birth on any of them: possibles to review, not a confirmed identity. |

POST /v1/criminal/search — sandbox

```bash
curl -X POST "https://sandbox.offendersearch.app/v1/criminal/search" \
  -H "X-API-Key: $OFFENDERSEARCH_TEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "query": {
        "firstName": "Felony",
        "lastName": "Record",
        "dob": "1975-06-15"
      }
    }'
```

In a batch, each row gets the same answer it gets from `POST /v1/criminal/search`, in input order.

POST /v1/criminal/batch — sandbox

```bash
curl -X POST "https://sandbox.offendersearch.app/v1/criminal/batch" \
  -H "X-API-Key: $OFFENDERSEARCH_TEST_KEY" \
  -H "Content-Type: application/json" \
  -d '[
      {
        "firstName": "Clear",
        "lastName": "Record"
      },
      {
        "firstName": "Felony",
        "lastName": "Record",
        "dob": "1975-06-15"
      }
    ]'
```

## Criminal search exclusion test names: Excluded, Sanctioned, Clear Provider

These names test exclusion-list records. Send each one with and without `include: ["exclusions"]` to see what a default criminal search returns and what the request adds. The `exclusion` block is defined on the [criminal Record object](https://offendersearch.app/docs/criminal/record-object.md).

| Send | What comes back |
| --- | --- |
| `firstName: Excluded`, `lastName: Provider`, `dob: 1968-03-22` | **Excluded provider, conviction-based.** One exclusion-list record (recordType healthcare_exclusion, currentStatus excluded): a Medicare/Medicaid exclusion under authority 1128(a)(1), a criminal conviction, so it is returned by default. The exclusion block carries the authority, convictionBased true and the exclusion date. DOB-verified. |
| `firstName: Sanctioned`, `lastName: Provider` | **Administrative exclusion, default search.** No records: this person's only entry is an administrative exclusion (a license suspension, not a conviction), which a search returns only when it sends include: ["exclusions"]. |
| `firstName: Sanctioned`, `lastName: Provider`, `include: exclusions` | **Administrative exclusion, include exclusions.** With include: ["exclusions"], the administrative exclusion comes back: convictionBased false, no date of birth on the list, so matchState no_dob_age_year and unverified true. counts.exclusionsIncluded is true. |
| `firstName: Clear`, `lastName: Provider`, `dob: 1980-01-01`, `include: exclusions` | **Clear exclusion check.** No match on any exclusion list, conviction-based or administrative: a complete answer with zero records. |

## Criminal reports, sources and jurisdictions in the sandbox

The rest of the Criminal Search API runs on the sandbox too, with the same paths and shapes as live. The test names above apply wherever a name is searched.

| Send | What comes back |
| --- | --- |
| `GET /v1/criminal/sources (no key), GET /v1/criminal/jurisdictions, POST /v1/criminal/resolve` | **Criminal sources, jurisdictions, resolve.** The production criminal service's own catalogue, jurisdiction list and website resolver, unchanged. |
| `POST /v1/criminal/report {"searchId": "<a sandbox criminal search id>"} (or an inline query)` | **Criminal verification report.** The production criminal report PDF for the sample records. Never billed. |

## Criminal search sandbox examples in cURL, Node and Python

Felony conviction: Felony Record 1975-06-15, called three ways. Export your test key as `OFFENDERSEARCH_TEST_KEY` first.

POST /v1/criminal/search — sandbox — cURL

```bash
curl -X POST "https://sandbox.offendersearch.app/v1/criminal/search" \
  -H "X-API-Key: $OFFENDERSEARCH_TEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "query": {
        "firstName": "Felony",
        "lastName": "Record",
        "dob": "1975-06-15"
      }
    }'
```

POST /v1/criminal/search — sandbox — Node

```javascript
// Node 18+ — fetch is built in.
const BASE = process.env.OFFENDERSEARCH_BASE ?? 'https://sandbox.offendersearch.app';

const res = await fetch(`${BASE}/v1/criminal/search`, {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.OFFENDERSEARCH_TEST_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "query": {
      "firstName": "Felony",
      "lastName": "Record",
      "dob": "1975-06-15"
    }
  }),
});
const body = await res.json();

console.log(res.headers.get('X-Offendersearch-Environment')); // "sandbox"
console.log(body.sandbox);                 // true
console.log(body.records[0].recordType);   // read it before describing the record
```

POST /v1/criminal/search — sandbox — Python

```python
import os
import requests

BASE = os.environ.get("OFFENDERSEARCH_BASE", "https://sandbox.offendersearch.app")

r = requests.post(
    f"{BASE}/v1/criminal/search",
    headers={"X-API-Key": os.environ["OFFENDERSEARCH_TEST_KEY"]},
    json={
        "query": {
            "firstName": "Felony",
            "lastName": "Record",
            "dob": "1975-06-15",
        },
    },
    timeout=30,
)
body = r.json()

print(r.headers["X-Offendersearch-Environment"])  # sandbox
print(body["sandbox"])                       # True
print(body["records"][0]["recordType"])
```

## Postman collection for the criminal search sandbox

One collection covers every API’s sandbox, with a folder per API, ready to import into Postman or Insomnia: https://offendersearch.app/downloads/offendersearch-sandbox.postman_collection.json. The **Criminal Search API** folder has one request for every scenario on this page. Set `{{apiKey}}` to your test key; `{{baseUrl}}` is preset to the sandbox. Ids from earlier responses go in `{{criminalSearchId}}`.

## Move criminal search from the sandbox to production

The sandbox and the live API differ only in the base URL and the key. Keep both in configuration, not in code, and going live is a configuration change:

.env

```bash
# Sandbox
OFFENDERSEARCH_BASE=https://sandbox.offendersearch.app
OFFENDERSEARCH_KEY=os_test_…

# Production — change these two values and nothing else
OFFENDERSEARCH_BASE=https://api.offendersearch.app
OFFENDERSEARCH_KEY=os_live_…
```

- Create a live key on the same [Developers → API keys](https://offendersearch.app/dashboard/keys) page.
- Remove any branch that reads `sandbox`. The field does not exist on live.
- Live answers are real people. Read [Matching & confidence](https://offendersearch.app/docs/criminal/matching.md) and [Completeness & source status](https://offendersearch.app/docs/criminal/result-completeness.md) before you decide what your interface shows.
- Standard criminal search results are not a consumer report. For hiring or tenant decisions, use the [FCRA API](https://offendersearch.app/docs/fcra.md).
- Live calls are billed; see [pricing](https://offendersearch.app/pricing).

---

## Related

- Previous: [Errors & status codes](https://offendersearch.app/docs/criminal/errors.md)
- Next: [Search - POST /v1/criminal/search](https://offendersearch.app/docs/criminal/search.md)
- Index: [Criminal Search API reference](https://offendersearch.app/docs/criminal.md)
