# Platform integration

> Build the Map API into a platform: secret or browser keys, X-End-User and limits for each of your users, ETag caching, and counting first, opening on tap.

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

## Map API platform integration: which key

A platform puts the Map API in front of many users of its own. The questions are which key makes the call, how each user is told apart, and how to keep calls few.

| Where the call is made | Key | Notes |
| --- | --- | --- |
| Your server (server to server) | Your secret key | Works on every route. Send `X-End-User` if you want per-user limits. |
| The browser, on your own site | A map browser key, `os_map_pk_…`, with its list of allowed sites | `X-End-User` is required on `POST /v1/map/route` and `POST /v1/map/routes`. |
| A phone app | Your secret key, on your server | A phone app has no web origin, so it calls through your server. |

See [Access & keys](https://offendersearch.app/docs/map/access.md) for creating a browser key and its allowed sites.

## X-End-User and limits for each end user

`X-End-User` is any stable id of yours for the person behind a call, such as a hashed user id. It is never shown to anyone. With it, each of your users has a share of the account, and one heavy user is stopped before the account is.

| Limit for each end user | Default | Past it |
| --- | --- | --- |
| Different map squares read for pins, each day | 2,000 | `429 map_end_user_limit` |
| Different routes checked, each day | 20 | `429 map_end_user_route_limit` |
| Different records opened, each day | Off; set on request | `429 map_open_limit` |

Other users of the account are not affected when one user reaches a limit. The account’s own limits are in [Daily limits](https://offendersearch.app/docs/map/limits-and-errors.md#daily-limits).

## Map API caching: ETag, If-None-Match and 304

- **Send `If-None-Match`.** Views and single routes carry an `ETag`. The same request with it answers `304`, with no body, and a `304` is free.
- **`Cache-Control: private, max-age=300`.** Keep an answer for 5 minutes for the user who asked. Do not put it in a shared cache.
- **Marker ids live 24 hours,** for your account only. Keep the ids of the answer you drew and reuse them: re-opening the same marker is free.
- **Counts for a fixed place** may be kept for 24 hours and shown to everyone. Pins and ids belong to one answer.

cache.js

```javascript
// Keep each answer with its ETag; ask again with If-None-Match.
const kept = new Map();  // route body -> { etag, answer }

async function callRoute(body) {
  const key = JSON.stringify(body);
  const seen = kept.get(key);
  const headers = { 'X-API-Key': KEY, 'X-End-User': userId, 'Content-Type': 'application/json' };
  if (seen) headers['If-None-Match'] = seen.etag;
  const res = await fetch(API + '/v1/map/route', { method: 'POST', headers, body: key });
  if (res.status === 304) return seen.answer;  // unchanged: free
  if (!res.ok) throw await res.json();
  const answer = await res.json();
  kept.set(key, { etag: res.headers.get('ETag'), answer });
  return answer;
}
```

## Count-first map API design: open on tap

- **Count first.** `detail=count` costs the same as a full answer, returns no pins and no ids, and is never refused by the area limits. It is the right call for a badge or a summary line.
- **Pins when asked.** Ask for `detail=full` when the user wants to see the map.
- **Open on tap.** Open a record only when a user taps a pin. Each person in each answer is one record open.

count-first.js

```javascript
// Count first: one route call with detail=count, no pins, the same price.
const count = await callRoute({ ...route, detail: 'count' });
showBadge(count.total);

// Pins only when the user asks to see them; records only when one is tapped.
showPinsButton.onclick = async () => {
  const full = await callRoute({ ...route, detail: 'full' });
  full.markers.forEach((m) => drawPin(m, () => openRecord(m.id)));
};
```

## Platform integration billing

Everything your users do is billed to your account: map requests and route units at $2.00 per 1,000 map requests, and record opens at your account’s rate, $0.035 by default. Daily limits for the account and for each end user can be set on request. See [Billing](https://offendersearch.app/docs/map/billing.md).

---

## Related

- Previous: [Walking routes](https://offendersearch.app/docs/map/walking-routes.md)
- Next: [MapLibre](https://offendersearch.app/docs/map/maplibre.md)
- Index: [Map API reference](https://offendersearch.app/docs/map.md)
