Truck isochrones, billed one call per contour

# Truck isochrones, billed one call per contour

`POST /isochrone` returns reachability contours: the area a vehicle can get to
from an origin inside a time or distance budget, as GeoJSON you can drop
straight onto a map. Send `"costing": "truck"` and the same height, width,
length and weight limits that shape a truck route shape the contour, so a
4-metre-high box van's 30-minute area stops at the low bridge instead of running
through it. The limits are enforced inside the costing model rather than
filtered off the result afterwards, so the polygon is an area the vehicle can
actually reach. Billing is one call per contour, capped at 10 contours per
request: 0.05p per contour on standard costing, 1p per contour when the body
carries truck costing.

## What does POST /isochrone actually return?

A GeoJSON `FeatureCollection` with one feature per contour, returned from the
routing engine. Each feature's `properties` carry the `contour` value in the
unit you asked for and a `metric` of `"time"` or `"distance"`.

Here is a real request. Where can a 4-metre-high, 40-tonne truck get to from a
London depot in 10 and 20 minutes:

```sh
export BASE=https://api.mapmap.ai
export API_KEY=snk_...

curl -fsS -X POST "$BASE/isochrone" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"locations": [{ "lat": 51.5074, "lon": -0.1278 }],
"costing": "truck",
"contours": [{ "time": 10 }, { "time": 20 }],
"polygons": true,
"costing_options": { "truck": { "height": 4.0, "weight": 40.0 } }
}'
```

And the response shape, truncated to one feature:

```json
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": { "contour": 10.0, "metric": "time" },
"geometry": {
"type": "Polygon",
"coordinates": [[[-0.14, 51.50], [-0.11, 51.50], [-0.11, 51.52], [-0.14, 51.50]]]
}
}
]
}
```

GeoJSON geometry is `[lon, lat]`. That is the GeoJSON standard, not our
choice. Note that request `locations` are `{lat, lon}` objects with named keys,
so there is no coordinate-order ambiguity on the way in.

The parameters worth knowing:

| Field | Required | What it does |
|---|---|---|
| `locations` | yes | Origins the reachability is computed from. |
| `costing` | yes | `auto`, `truck`, `bus`, `motor_scooter`, `motorcycle`, `bicycle` or `pedestrian`. |
| `contours` | yes | Each is a `time` in minutes or a `distance` in kilometres. A contour carrying neither is a `400`. |
| `polygons` | no | Return polygons instead of the default linestrings. |
| `denoise` | no | `0` to `1`. Higher drops smaller contour islands. |
| `generalize` | no | Geometry simplification tolerance in metres. |
| `show_locations` | no | Include the input locations as GeoJSON points. |
| `costing_options` | no | Passed to the engine verbatim, keyed by costing name. |

## What do fleets actually use reachability polygons for?

Depot siting, service-area definition and driver-hours planning. None of the
three is drawing a pretty blob on a map.

- **Depot siting.** You have a candidate site and a list of customers. The
question is not "how far is it" but "how many of these can a 40-tonne artic
reach inside a 45-minute window". Run the contour from each candidate, count
the customers inside, compare. Straight-line radius answers a different
question and answers it wrongly, because the truck drives roads, not circles.
- **Service-area definition.** "Can we serve this postcode in under 45 minutes"
is a contract question, and a sales team asking it 200 times a day wants a
polygon they can test a point against rather than 200 route calls. One
isochrone, cached, does the work of a lot of routing.
- **Driver-hours planning.** A driver has a legal driving window left. The
contour is the honest answer to what is still reachable inside it, computed
against the roads the vehicle can legally use.

For the many-to-many version of the depot question, where you want the actual
travel time from each of 12 depots to each of 40 stops rather than an area, use
`POST /matrix` instead. We wrote that up in
[the matrix API and how it bills by element](/news/matrix-api). The rule of
thumb: an isochrone answers "what is in range", a matrix answers "how far is
each of these, exactly".

## Why is a truck isochrone a different shape from a car isochrone?

Because `costing_options` reaches the engine verbatim, so the truck cost model
that gates a route gates the contour too. A low bridge, a weight-limited lane
and a width restriction are edges the search cannot traverse under truck
costing, and an area computed from a search that cannot traverse them is
therefore an area the truck can actually reach.

```json
{
"truck": {
"height": 4.0, "width": 2.55, "length": 16.5,
"weight": 40.0, "axle_load": 9.0, "axle_count": 5,
"hazmat": true, "adr_tunnel_code": "C"
}
}
```

Dimensions in metres, weights in tonnes. `adr_tunnel_code` is honoured by our
ADR-extended engine, so a hazmat contour can be tightened by the tunnels the
load may not enter. A stock engine ignores unknown keys, which is why the
request stays portable.

Be careful with what `adr_tunnel_code` in `costing_options.truck` does and does
not promise. The isochrone is shaped by ADR costing where the engine supports
it, but the isochrone is not a compliance product and we would rather say so
here than let you find out later.
`POST /adr/check` is the authoritative tunnel-entry decision, and it is the one
to log. Do not build a dangerous-goods compliance step on top of a polygon.

## What does an isochrone cost?

One call per contour, and the class is decided from the body.

| Request | Units | Class | Cost |
|---|---|---|---|
| 1 contour, `"costing": "auto"` | 1 | Standard | 0.05p |
| 3 contours, `"costing": "auto"` | 3 | Standard | 0.15p |
| 3 contours, `"costing": "truck"` | 3 | Premium | 3p |
| 10 contours, `"costing": "truck"` | 10 | Premium | 10p |
| 11 contours | rejected | n/a | `400`, nothing billed |

Standard is 0.05p per call up to 1M calls a month, 0.03p to 10M, 0.02p beyond.
Premium is 1p, 0.7p and 0.4p across the same bands. Bands are marginal, so each
price applies only to the calls inside it.

The free tier is denominated in standard calls: 50,000 a month after email
verification, and a premium call draws 20 of them. A 3-contour truck isochrone
therefore draws 60 included calls, which is worth knowing before you put one
behind a page load. Prices reconcile with
[/pricing.json](https://mapmap.ai/pricing.json), which wins if this page ever
drifts.

## What this does not do

Five real limits, in writing.

- **A top-level `adr` object is ignored here, and still bills premium.** The
`adr` extension is honoured on `POST /route`, `POST /route/along` and
`POST /optimise` only. The
isochrone request has no such field, so it is dropped silently by the handler,
while the billing classifier sees it and charges premium. You pay 1p a contour
for something that had no effect. Put truck and ADR settings in
`costing_options.truck` instead, which is the path that actually reaches the
engine.
- **10 contours is a hard cap.** An 11th contour is a `400` and the request is
not billed. Split it.
- **GraphHopper deployments are more limited.** On a self-hosted deployment
running `SN_ROUTING_ENGINE=graphhopper`, isochrones are single-origin, cannot
mix time and distance contours in one call, and match your contours exactly
only when they are evenly spaced. Valhalla, the default, has none of these
limits.
- **The MCP isochrone tool is narrower than the endpoint.** `reachable_area`
on our MCP server computes time contours from a single origin, in minutes
only, with no `costing_options`. For distance contours, multiple origins or
truck dimensions, an agent has to call the HTTP endpoint directly.
- **A contour is a model, not a promise.** It is computed from the road network
and the costing model, and it does not know that your yard gate is locked at
18:00 or that the customer needs a 20-minute unload. Reachability is not
serviceability.

## Try it

The full parameter table, the error envelope and the sibling endpoints are in
[the analysis APIs documentation](/docs/analysis), which is authoritative on
request and response shapes. A key comes from one card-free call to
`POST /v1/keys` and works immediately, and the
[playground](/playground) runs the request in a browser with no install.

Routing derives from OpenStreetMap, so credit "© OpenStreetMap contributors"
when you render or republish it.