# Map API sandbox server & test keys

> Build on the Map API sandbox server with an os_test_ key and no live key: every route, fixed made-up people, no plan to turn on and nothing metered.

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

## Map API test keys: create one in the dashboard

The Map API sandbox server is the Map API on a separate server, for building with a test key and no live key at all. Every route runs there with no plan to turn on and nothing metered, and every request is in test mode. Already calling the live API? Add `sandbox=true` instead: see [Test mode](https://offendersearch.app/docs/map/test-mode.md). Both draw the same made-up people.

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.

## Map API sandbox base URL vs live and test mode

|  | Sandbox | Production |
| --- | --- | --- |
| Base URL | `https://sandbox.offendersearch.app` | `https://api.offendersearch.app` |
| Key prefix | `os_test_` | `os_live_` |
| Data | The made-up test-mode people, around eight cities | Live registry data, once the Map API is turned on |
| 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).

## Map API sandbox scenarios

Every [Map API](https://offendersearch.app/docs/map.md) route runs on the sandbox, with no plan to turn on and nothing metered. It draws the same made-up people as [test mode](https://offendersearch.app/docs/map/test-mode.md) on the live API, around Columbus, Cleveland, Cincinnati, Chicago, New York, Los Angeles, Houston and Seattle. You do not add `sandbox=true` on the sandbox: every request there is test mode. The counts below are fixed.

| Send | What comes back |
| --- | --- |
| `GET /v1/map/viewport`, `bbox=-83.20,39.85,-82.80,40.10` | **A city view: clusters.** Zoom 11 over Columbus, OH: mode clusters, 529 people in 63 clusters. |
| `GET /v1/map/viewport`, `center=39.9612,-82.9988`, `radiusMiles=1` | **One mile around a point: named pins.** Zoom 15: mode markers, exactly 37 people within the mile (withinRadius 37), 15 named pins plus one approximate-area pin, each with distanceMiles. |
| `GET /v1/map/viewport`, `bbox=-83.0060,39.9665,-83.0025,39.9690` | **Many people at one address.** One street-precision pin holding 14 people (a groupId, no inline members); GET /v1/map/groups/{groupId} pages through the 14, all housingStatus homeless. |
| `GET /v1/map/viewport`, `bbox=-83.0130,39.9680,-83.0100,39.9700` | **An approximate-area pin.** No street pins; one area pin (precision zip, label 43215) for 9 people whose registry gives only a ZIP code. |
| `POST /v1/map/area` | **A drawn polygon.** shape polygon: 22 people inside, 9 pins (the 14-person address is one of them). |
| `GET /v1/map/records/{markerId}`, `markerId from any named pin of map-radius-pins` | **Open the record behind a pin.** The complete record (a 'Sample …' person, Sampleville, ZIP 00000), a recordRef that opens it again later, and billing {billed: false, reason: sandbox}. |
| `POST /v1/map/route` | **A walking route with a stop.** 2.118 miles, 4 route units: 2 people within the 0.1-mile corridor, 1 within 1,000 feet of stop s1, and 9 more placed only to a ZIP (approximate.count 9). |
| `POST /v1/map/routes` | **Two routes in one call.** shape routes, 8 units, uniquePeople 2 (the same two people on both routes). |
| `GET /v1/map/status` | **Plan and usage.** enabled true, sandbox true, the published rates, and zero usage this month. |
| `POST /v1/map/enable` | **Turning it on or off.** The sandbox map is always on: enable and disable both answer the status with changed false, and nothing is charged. |

GET /v1/map/viewport — sandbox

```bash
curl "https://sandbox.offendersearch.app/v1/map/viewport?bbox=-83.20,39.85,-82.80,40.10" \
  -H "X-API-Key: $OFFENDERSEARCH_TEST_KEY"
```

## Map API sandbox examples in cURL, Node and Python

A city view: clusters, called three ways. No sandbox=true is needed on the sandbox server. Export your test key as `OFFENDERSEARCH_TEST_KEY` first.

GET /v1/map/viewport — sandbox — cURL

```bash
curl "https://sandbox.offendersearch.app/v1/map/viewport?bbox=-83.20,39.85,-82.80,40.10" \
  -H "X-API-Key: $OFFENDERSEARCH_TEST_KEY"
```

GET /v1/map/viewport — 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/map/viewport?${new URLSearchParams({"bbox":"-83.20,39.85,-82.80,40.10"})}`, {
  headers: {
    'X-API-Key': process.env.OFFENDERSEARCH_TEST_KEY,
  },
});
const body = await res.json();

console.log(res.headers.get('X-Offendersearch-Environment')); // "sandbox"
console.log(body.sandbox);  // true
console.log(body.mode);
```

GET /v1/map/viewport — sandbox — Python

```python
import os
import requests

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

r = requests.get(
    f"{BASE}/v1/map/viewport",
    headers={"X-API-Key": os.environ["OFFENDERSEARCH_TEST_KEY"]},
    params={
        "bbox": "-83.20,39.85,-82.80,40.10",
    },
    timeout=30,
)
body = r.json()

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

## Postman collection for the Map API 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 **Offender Map 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 `{{markerId}}`.

## Move the Map API 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.
- Turn the Map API on for the live account: see [Turn it on](https://offendersearch.app/docs/map/turn-it-on.md). Live map requests and record opens are billed; see [Billing](https://offendersearch.app/docs/map/billing.md).
- For a browser map, create a map browser key for your sites: see [Access & keys](https://offendersearch.app/docs/map/access.md).

---

## Related

- Previous: [Test mode](https://offendersearch.app/docs/map/test-mode.md)
- Next: [The viewport request](https://offendersearch.app/docs/map/viewport.md)
- Index: [Map API reference](https://offendersearch.app/docs/map.md)
