# The viewport request

> Every parameter of the viewport route: a box or a centre and radius, how the zoom picks clusters, pins or named pins, the pin cap, and the caching headers.

- **HTML:** https://offendersearch.app/docs/map/viewport
- **Base URL:** https://api.offendersearch.app
- **Authentication:** `X-API-Key` request header
- **Map API reference as markdown:** https://offendersearch.app/docs/map.md

## The request

`GET /v1/map/viewport` answers one map view. Call it when the map first loads and each time it stops moving. One call is one map request.

GET /v1/map/viewport

```bash
curl -G https://api.offendersearch.app/v1/map/viewport \
  -H "X-API-Key: $OFFENDERSEARCH_KEY" \
  -d bbox=-83.2,39.8,-82.8,40.1 \
  -d zoom=10
```

## Parameters

| Parameter | Meaning |
| --- | --- |
| `bbox` | `west,south,east,north` in degrees. Any size. A box that crosses the date line has west greater than east. |
| `center` + `radiusMiles` | Instead of `bbox`: `lat,lng` and a distance from 0.1 to 100 miles. See [Near an address](https://offendersearch.app/docs/map/near-an-address.md). |
| `zoom` | Optional. 0 to 22; a fraction is rounded down. Send the map’s current zoom. |
| `format` | `json` (the default) or `geojson`. |
| `detail` | `full` (the default) or `count` for numbers only. |
| `housing` | `housed`, `homeless`, `incarcerated`. Separate several with commas. |
| `precision` | `street` returns only people with an exact street address. `any` (the default) returns everyone. |
| `sandbox` | `true` puts the request in [test mode](https://offendersearch.app/docs/map/test-mode.md). |

Send `bbox`, or `center` with `radiusMiles`. Not both.

The optional `X-End-User` header carries any id you choose for the person using the map. It is read on this route and on `POST /v1/map/area`. See [Limits & errors](https://offendersearch.app/docs/map/limits-and-errors.md).

## How the zoom picks the detail level

| Effective zoom | `mode` | Returned |
| --- | --- | --- |
| 3–12 | `clusters` | A count for each grid cell, with its extent and a breakdown. |
| 13–14 | `pins` | One pin for each address: a position, with no name. |
| 15+ | `markers` | Pins with a name label and basic fields. |

The effective zoom is worked out like this:

1. It starts as the `zoom` you sent. If you sent none, it is the zoom at which the box is about four map tiles wide.
2. If the box covers more than 64 map tiles at that zoom, the zoom is lowered until it fits.
3. It is never lower than 3.

The response states the zoom that was used, in `zoom`, and the level, in `mode`. Draw from `mode`, not from your own zoom.

## Any size of area

There is no largest box. A view of a whole country is answered as clusters, in one map request, at the same price as a view of one street. A wide box is simply answered at a lower zoom.

## The 300-pin rule

A response holds at most 300 pins. A view at zoom 13 or closer with more than that is answered as clusters on a finer grid, with `mode: "clusters"`. Each cluster’s `expansionZoom` says how far to zoom for it to come apart.

## The response

200 OK

```json
{
  "mode": "clusters",
  "zoom": 10,
  "bbox": [-83.2, 39.8, -82.8, 40.1],
  "datasetVersion": "2026-10-12",
  "total": 508,
  "breakdown": {
    "street": 508, "housed": 247, "homeless": 96, "incarcerated": 0
  },
  "clusters": [
    {
      "id": "c10_1103_1551",
      "lat": 39.9612, "lng": -82.9988,
      "count": 412,
      "bbox": [-83.06, 39.91, -82.97, 39.98],
      "expansionZoom": 11,
      "breakdown": {
        "street": 412, "housed": 201, "homeless": 77, "incarcerated": 0
      }
    }
  ],
  "markers": [],
  "areas": [],
  "unplaced": {"count": 96, "states": ["OH"]},
  "notice": "A pin shows a registered address, not a person's current location.",
  "sandbox": true
}
```

| Field | Meaning |
| --- | --- |
| `mode` | `clusters`, `pins`, `markers`, or `count` when `detail=count`. |
| `zoom` | The effective zoom the view was answered at. |
| `bbox` | The box that was answered. |
| `datasetVersion` | The version of the map data that answered. |
| `total` | People in the view. In a clusters answer: in the clusters returned. With a radius: people inside the circle. |
| `breakdown` | Of `total`: how many have an exact street address, and the housing status where the registry states it. |
| `clusters`, `markers`, `areas` | What to draw. See [Clusters & markers](https://offendersearch.app/docs/map/clusters-and-markers.md). |
| `unplaced` | People in the states this view touches who have no mappable address. |
| `notice` | A sentence to show on the map. |
| `center`, `radiusMiles`, `withinRadius` | Only when the request used a radius. `withinRadius` is exact. |

Markers are sorted by distance from the centre of the view.

## Caching

Every answer carries `Cache-Control: private, max-age=300` and an `ETag`.

- Send the `ETag` back in `If-None-Match`. An unchanged view answers `304`, with no body. **A `304` is not billed.**
- The `ETag` changes when the map data changes, and at least every 12 hours, so marker ids are never kept past their life.
- Keep the body with its `ETag`. On a `304`, draw the body you kept.
- The answer is private to your account. Do not put it in a shared cache.

## Errors

A bad parameter answers `422` with a code that names it: `invalid_bbox`, `invalid_center`, `invalid_radius`, `invalid_zoom`, `invalid_filter`, `invalid_format`, `invalid_detail`. All codes are in [Limits & errors](https://offendersearch.app/docs/map/limits-and-errors.md).

---

## Related

- Previous: [Test mode](https://offendersearch.app/docs/map/test-mode.md)
- Next: [Clusters & markers](https://offendersearch.app/docs/map/clusters-and-markers.md)
- Index: [Map API reference](https://offendersearch.app/docs/map.md)
