# Analysis APIs: isochrones, matrices, along-route search, map matching, safety cameras, incidents & weather

Eight endpoints answer the fleet questions that sit around routing rather
than on it: **where can this vehicle get to in 30 minutes?**
(`POST /isochrone`), **what are the travel times between every depot and
every stop?** (`POST /matrix`), **what can I stop at on the way, and what
does the detour really cost?** (`POST /route/along`), **which roads
did this GPS trace actually drive?** (`POST /trace_route`,
`POST /trace_attributes`), **which safety cameras sit on this
route?** (`POST /v1/cameras/along`), **which closures or lane
restrictions sit on this route?** (`POST /v1/incidents/along`), and
**what will the weather be doing when I actually get there?**
(`POST /v1/weather/along`). The first five
accept the same costing models as `POST /route`, including truck costing
with dimensional and ADR constraints (ADR is the European agreement on
carriage of dangerous goods by road; tunnel codes B–E restrict which
tunnels a hazmat load may use; see [conventions](/docs/conventions) for
the full table). An isochrone for a 44-tonne hazmat artic is the area that
lorry can *legally* reach, not the area a car could.

## Availability

**Status, honestly:** the hosted gateway at `https://api.mapmap.ai` is live:
[sign up](/signup) for a key, or run the [self-host distro](/docs/self-host).
Base URL and authentication conventions are on the
[conventions page](/docs/conventions).

## Shared conventions

- You need an API key (`Authorization: Bearer snk_…`; get one in the
  [quickstart](/docs/quickstart)).
- Locations everywhere on this page are `{ "lat": …, "lon": … }` objects
  with named keys, so there is no coordinate-order ambiguity.
- `costing` is one of `auto`, `truck`, `bus`, `motor_scooter`,
  `motorcycle`, `bicycle`, `pedestrian`.
- `costing_options` is passed to the engine verbatim, keyed by costing
  name. For trucks: `{"truck": {"height": 4.0, "width": 2.55, "length":
  16.5, "weight": 40.0, "axle_load": 9.0, "axle_count": 5, "hazmat": true,
  "top_speed": 90.0, "adr_tunnel_code": "C"}}`; dimensions in metres,
  weights in tonnes, speed in km/h. `adr_tunnel_code` is honoured by our
  ADR-extended engine; a stock engine ignores unknown keys, so requests
  stay portable.
- Requests bill by size: a matrix bills one call per started block of 25
  elements (`sources × targets`), capped at 10,000 elements per request;
  an isochrone bills one call per contour (max 10 per request); an
  along-route search bills like a matrix on its detour budget
  (`4 × max_detours` elements, max 25 detours), plus one call when it
  computes the route for you. The class is **Standard** normally,
  **Premium** when the body carries `"costing": "truck"`. See
  [pricing](/pricing) for the per-class rates.

## Isochrones: `POST /isochrone`

Reachability contours from an origin: how far can you get within a time or
distance budget. Use it for delivery-coverage maps, depot placement,
"can we serve this postcode in under 45 minutes?" checks, and driver-hours
planning. The response is GeoJSON you can drop straight onto a map.

### Parameters

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `locations` | array of `{lat, lon}` | yes (≥ 1) | Origin(s) the reachability is computed from. |
| `costing` | string | yes | Costing model (see shared conventions). |
| `contours` | array | yes (≥ 1) | Each contour is a `time` in **minutes** or a `distance` in **kilometres**; a contour with neither is a `400`. |
| `polygons` | boolean | no | Return polygons instead of the default linestrings. |
| `denoise` | number | no | `0`–`1`; higher drops smaller contour islands. |
| `generalize` | number | no | Geometry simplification tolerance in metres. |
| `show_locations` | boolean | no | Include the input locations as GeoJSON points in the response. |
| `costing_options` | object | no | Verbatim costing options, e.g. `{"truck": {…}}`. |
| `id` | string | no | Opaque identifier echoed back. |

### Example

Where can a 4-metre-high 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 } }
}'
```

### Response (truncated)

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

```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 itself is `[lon, lat]`; that is the GeoJSON standard,
not ours.)

**Deployment note:** on self-hosted deployments running the GraphHopper
engine (`SN_ROUTING_ENGINE=graphhopper`), isochrones are single-origin,
cannot mix time and distance contours in one call, and the rings match
your contours exactly only when they are evenly spaced. The Valhalla
engine (the default) has none of these limits.

## Matrix: `POST /matrix`

A many-to-many travel-time and distance matrix: `sources` are the rows,
`targets` the columns. This is the workhorse behind dispatch decisions
("which of my 12 drivers is genuinely closest, by road, in a lorry?") and
the same computation [`POST /optimise`](/docs/optimisation) runs
internally. Call it directly when you want the raw numbers for your own
solver or ranking logic.

### Parameters

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `sources` | array of `{lat, lon}` | yes (≥ 1) | Row locations (origins). |
| `targets` | array of `{lat, lon}` | yes (≥ 1) | Column locations (destinations). |
| `costing` | string | yes | Costing model (see shared conventions). |
| `costing_options` | object | no | Verbatim costing options, e.g. `{"truck": {…}}`. |
| `units` | string | no | `kilometers` or `miles`; accepted, but the gateway normalises response distances to **metres** regardless. |
| `id` | string | no | Opaque identifier echoed back. |

### Example

One depot to two delivery cities, truck costing:

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

curl -fsS -X POST "$BASE/matrix" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "sources": [{ "lat": 51.5074, "lon": -0.1278 }],
  "targets": [
    { "lat": 52.4862, "lon": -1.8904 },
    { "lat": 53.4808, "lon": -2.2426 }
  ],
  "costing": "truck",
  "costing_options": { "truck": { "height": 4.0, "weight": 40.0 } }
}'
```

### Response

Normalised and complete: this is the whole shape. Row-major matrices:
`durations` in **seconds**, `distances` in **metres**, `null` where a pair
is unreachable under the chosen costing:

```json
{
  "durations": [[9000.0, 12500.0]],
  "distances": [[171100.0, 262000.0]]
}
```

`durations[i][j]` is from `sources[i]` to `targets[j]`. Unlike
[`/optimise`](/docs/optimisation), unreachable pairs here are honest
`null`s, not sentinel costs.

## Along-route search: `POST /route/along`

"Where can my driver get diesel on the way, and what does the stop
*really* cost?" This endpoint searches **your own places** (the dataset
you uploaded to [`/places`](/docs/api-reference)) along a route, and
ranks every candidate by **honest detour cost**: for each place inside the
corridor, the routing engine measures leave-route → place → rejoin-route
against staying on the route, so `detour_seconds` is real added driving
time under your costing (a truck's detour, not a car's), never
straight-line optimism. One primitive covers diesel, truck parking and any
consumer POI: "coffee, adds 4 min".

Give it either a full route request (`route`, the same body as
`POST /route`; the gateway computes the route for you) or an existing
geometry (`shape`, polyline6 from a `/route` response leg). A cheap
corridor pre-filter runs server-side first, so only the `max_detours`
nearest candidates ever reach the matrix engine.

### Parameters

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `route` | object | one of `route` / `shape` | A Valhalla-style route request (same body as `POST /route`, including the truck/ADR options). Detour matrices reuse its costing. |
| `shape` | string | one of `route` / `shape` | An existing route geometry as an encoded polyline with **six** digits of precision (polyline6). |
| `costing` | string | no | `shape` mode only (default `auto`): costing model for the detour measurements. |
| `costing_options` | object | no | `shape` mode only: verbatim costing options, e.g. `{"truck": {…}}`. |
| `category` | string | no | Only places carrying this category (case-insensitive), e.g. `"diesel"`, `"truck_parking"`, `"coffee"`. |
| `corridor_m` | number | no | Corridor half-width in metres, 50–10,000 (default 2,000). |
| `max_detours` | integer | no | Candidates ranked by detour cost, 1–25 (default 10); also the billing budget. |

### Example

Truck parking within 5 km of a Dover → Birmingham truck route:

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

curl -fsS -X POST "$BASE/route/along" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "route": {
    "locations": [
      { "lat": 51.1279, "lon": 1.3134 },
      { "lat": 52.4862, "lon": -1.8904 }
    ],
    "costing": "truck",
    "costing_options": { "truck": { "height": 4.0, "weight": 40.0 } }
  },
  "category": "truck_parking",
  "corridor_m": 5000,
  "max_detours": 10
}'
```

### Response

Hits ranked by `detour_seconds`, cheapest stop first. `along_fraction` is
how far along the route the stop sits (0 = origin, 1 = destination);
`offset_m` its straight-line distance from the route; `unreachable` counts
in-corridor candidates the engine could not connect:

```json
{
  "count": 2,
  "corridor_m": 5000.0,
  "route_duration_s": 10620.0,
  "route_distance_m": 273400.0,
  "unreachable": 0,
  "results": [
    {
      "place": {
        "id": "tp-ashford", "name": "Ashford Truckstop",
        "lat": 51.13, "lon": 0.85, "categories": ["truck_parking"]
      },
      "detour_seconds": 240.0,
      "detour_km": 2.6,
      "along_fraction": 0.18,
      "offset_m": 850.0
    },
    {
      "place": {
        "id": "tp-oxford", "name": "Oxford Services",
        "lat": 51.83, "lon": -1.19, "categories": ["truck_parking"]
      },
      "detour_seconds": 660.0,
      "detour_km": 8.9,
      "along_fraction": 0.71,
      "offset_m": 2300.0
    }
  ]
}
```

No dataset uploaded yet (or nothing inside the corridor) is a
well-formed empty result (`count: 0`), not an error. A `404` means the
deployment has no customer-places storage configured (`SN_PLACES_DIR` on
self-host; always on for the hosted gateway). Agents get the same
capability as the [MCP server's](/docs/mcp) `search_along_route` tool.

## Map matching: `POST /trace_route` and `POST /trace_attributes`

Both snap a recorded GPS trace to the road network. `/trace_route` returns
a turn-by-turn route, the same response shape as `POST /route` (see the
[API reference](/docs/api-reference)), for cleaning up noisy telematics
tracks into displayable routes. `/trace_attributes` returns the matched
road segments and their attributes instead, for questions like "did this
vehicle use the motorway?" or mileage auditing by road actually driven.

Both take the same request body:

### Parameters

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `shape` | array of `{lat, lon}` | one of `shape` / `encoded_polyline` | The GPS trace as an ordered list of points. |
| `encoded_polyline` | string | one of `shape` / `encoded_polyline` | The trace as an encoded polyline with **six** digits of precision (polyline6). |
| `costing` | string | yes | Costing model (see shared conventions). |
| `shape_match` | string | no | `edge_walk` (exact edge sequence, no snapping), `map_snap` (snap each point to the most likely edge), or `walk_or_snap` (try the first, fall back to the second; the default). |
| `filters` | object | no | `/trace_attributes` only: which edge attributes to include or exclude. |
| `units` | string | no | `kilometers` (default) or `miles`, for response `length` fields. |
| `costing_options` | object | no | Verbatim costing options. |
| `id` | string | no | Opaque identifier echoed back. |

### Example

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

curl -fsS -X POST "$BASE/trace_route" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "shape": [
    { "lat": 51.5074, "lon": -0.1278 },
    { "lat": 51.5090, "lon": -0.1300 }
  ],
  "costing": "auto",
  "shape_match": "map_snap"
}'
```

### Response (truncated)

`/trace_route` returns the standard route response: durations in seconds,
`length` in the trip's `units` (kilometres by default), leg `shape` as
polyline6:

```json
{
  "trip": {
    "locations": [
      { "lat": 51.5074, "lon": -0.1278 },
      { "lat": 51.5090, "lon": -0.1300 }
    ],
    "legs": [{
      "maneuvers": [
        { "type": 1, "instruction": "Drive north on High Street.",
          "time": 240.0, "length": 0.4,
          "begin_shape_index": 0, "end_shape_index": 2 }
      ],
      "summary": { "time": 240.0, "length": 0.4 },
      "shape": "}~ycbBhavgN..."
    }],
    "summary": { "time": 240.0, "length": 0.4 },
    "status": 0,
    "status_message": "Found route between points",
    "units": "kilometers",
    "language": "en-US"
  }
}
```

`/trace_attributes` with the same body returns an open JSON document from
the engine: the matched geometry plus per-edge attributes. The exact
attribute set depends on the engine and your `filters`; expect `shape`
(polyline6), `matched_points` and an `edges` array:

```json
{
  "shape": "}~ycbBhavgN...",
  "matched_points": [
    { "lat": 51.5074, "lon": -0.1278 },
    { "lat": 51.5090, "lon": -0.1300 }
  ],
  "edges": [
    { "way_id": 12345, "speed": 48 }
  ],
  "units": "kilometers"
}
```

A request with neither a non-empty `shape` nor an `encoded_polyline` is a
`400`.

## Safety cameras along a route (`POST /v1/cameras/along`)

Given a route shape, returns the safety cameras on it: where each camera
sits along the route, its type, and the enforced limit where known. Built
for navigation clients that warn the driver on approach. The data is
baked per territory at build time, and exact positions are served only
where that is lawful (see the jurisdiction policy below).

Two shapes of record come back. **Point devices** are a single camera at
a position. **Corridors** are a stretch of enforced road, either
average-speed section control or one of Ireland's published mobile-camera
zones; they carry a `zone` object and the arc offsets of both ends. A
corridor is always one entry, never one per device.

### Parameters

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `shape` | string | yes | The route geometry as an encoded polyline with **six** digits of precision (polyline6), exactly as `POST /route` responses emit their leg `shape`. |
| `buffer_m` | number | no | Corridor half-width in metres for matching cameras to the shape. Default 40; values above 100 are clamped to 100; zero, negative or non-numeric is a `400`. |

### Example

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

curl -fsS -X POST "$BASE/v1/cameras/along" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "shape": "}~ycbBhavgN...",
  "buffer_m": 40
}'
```

### Response

```json
{
  "cameras": [
    { "arc_m": 1234.5, "lat": 51.5074, "lon": -0.1278,
      "kind": "fixed", "limit_kph": 48, "bearing": null,
      "limit_confidence": "osm",
      "source": "osm", "licence": "ODbL-1.0" },
    { "arc_m": 4820.0, "lat": 53.3226, "lon": -6.4235,
      "kind": "mobile_site", "limit_kph": null, "bearing": null,
      "limit_confidence": "unknown",
      "from_arc_m": 4820.0, "to_arc_m": 6320.0,
      "zone": {
        "start": [53.322565, -6.423538],
        "end": [53.329367, -6.42377],
        "length_km": 1.5,
        "geometry": [[-6.423538, 53.322565], [-6.42377, 53.329367]],
        "geometry_parts": [2]
      },
      "source": "ie-garda-current-zones", "licence": "CC-BY-4.0" }
  ],
  "attribution": [
    "Contains Irish Public Sector Data licensed under a Creative Commons Attribution 4.0 International (CC BY 4.0) licence."
  ],
  "policy": { "country": "IE", "mode": "exact" }
}
```

- `cameras` is ordered by `arc_m`, the distance in metres along the
  shape to each camera's on-route projection. For a corridor that is its
  start, so a client that only understands points needs no new code.
- `kind` is `fixed`, `average` (a section-control camera), `redlight`,
  `mobile_site` (a published mobile-enforcement zone) or `unknown`.
- `limit_kph` is the enforced limit where the data records one, on the
  camera itself or its enforcement zone. **`null` is common and honest**
  rather than an error: Ireland's published zones carry no limit at all.
  Never substitute a guess.
- `limit_confidence` says how far `limit_kph` can be trusted:
  `official` (from the enforcing authority), `osm` (from map tagging),
  `inferred_from_road`, or `unknown`; always `unknown` when `limit_kph`
  is `null`.
- `bearing`, when present, is the camera facing in degrees clockwise
  from north. Cameras facing away from the direction of travel (more
  than 60 degrees off the local route bearing, typically the opposite
  carriageway) are filtered out server-side. Corridors carry no bearing:
  they are enforced in both directions.
- `attribution` lists the notices owed by the data actually returned.
  Display them verbatim. Empty when no cameras were returned.
- `source` and `licence` identify where each record came from, so mixed
  responses stay auditable.

#### Corridor records

Corridor entries (`average` section control and `mobile_site` zones) add:

| Field | Type | Description |
| --- | --- | --- |
| `zone.start` / `zone.end` | `[lat, lon]` | The corridor's published ends. The direction carries no enforcement meaning. |
| `zone.length_km` | number | Enforced length. Published where the authority states one, otherwise measured along the geometry. |
| `zone.geometry` | `[[lon, lat], …]` | The corridor polyline, in GeoJSON coordinate order. |
| `zone.geometry_parts` | `[number, …]` | Vertex count of each source part. **Split `geometry` on these boundaries when drawing**; joining the parts would draw connecting segments that are not in the data. |
| `from_arc_m` / `to_arc_m` | number | Where the corridor starts and ends along *this* route, in metres. Use the interval between them, with `zone.length_km` and `limit_kph`, to run an average-speed calculation client-side. `to_arc_m` can be the smaller of the two when the route runs against the corridor's published direction. |

One corridor is one entry. Do not expect one entry per camera on a
section-control stretch, and do not alert per device: that would warn the
driver several times for a single enforcement event.

### Jurisdiction policy

Camera-position warnings are illegal in some countries, so the gateway
applies a per-country policy server-side, before the response is built,
and reports what it did in `policy`:

| `mode` | Behaviour |
| --- | --- |
| `exact` | Exact camera positions are returned. |
| `omitted` | The `cameras` array is empty; only the policy object is returned. |

The default is **off**: only countries on an explicit, legally reviewed
allowlist (currently the United Kingdom and Ireland) return `exact`.
Every other jurisdiction returns `omitted`, researched prohibitions
(France, Germany, Switzerland, Austria and others) and unresearched
countries alike. On routes that cross borders, each camera is also gated
on its own country, so a prohibited neighbour's cameras never appear.
Clients must not work around the policy with their own camera data.

### Availability and billing

Bills as one **Standard** call, never Premium: it is a safety feature
and carries no extra charge. On deployments without camera data the
endpoint answers `501` (`urn:sn-gateway:problem:cameras-not-enabled`);
treat that as "feature not available here" and disable camera alerts,
rather than as an error. Self-hosters enable it by pointing
`SN_CAMERAS_DIR` at the per-territory `cameras.json` sidecars the
factory's `safety-cameras` stage produces.

### Data sources and licences

Camera data is either OpenStreetMap-derived or published by the enforcing
authority, and within one territory it is always one or the other, never
a mixture. That separation is a licence requirement, not a preference:
combining OSM and non-OSM records of the same feature type over the same
area makes the whole dataset an ODbL derivative with share-alike.

| Territory | Source | Licence |
| --- | --- | --- |
| Ireland | An Garda Síochána mobile safety camera zones (1,916 corridors) plus the published fixed and average-speed cameras (11 devices). OSM camera nodes are excluded. | CC BY 4.0 (Irish public sector information) |
| Everywhere else | OpenStreetMap | ODbL 1.0 |

The `attribution` array in every response carries the notices the
returned data obliges. Display them verbatim.

**Ireland publishes no speed limits.** Every Irish record has
`limit_kph: null` with `limit_confidence: "unknown"`, and the API will
not infer one from the road: Ireland re-based its default rural limit to
60 km/h in February 2025, so an inference tuned to the old defaults would
be wrong across a large part of the network. Clients must not fill the
gap with a guess of their own either.

## Traffic incidents along a route (`POST /v1/incidents/along`)

Given a route shape, returns the closures and lane restrictions on it:
where each one sits along the route, its cause and severity, and the
lane/time detail the source publishes. Built for the same "warn the
driver on approach" use case as `/v1/cameras/along`, over a different
data source: the National Highways *Road and Lane Closures* feed.

### Parameters

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `shape` | string | yes | The route geometry as a polyline6 string, exactly as `POST /route` responses emit their leg `shape`. |
| `buffer_m` | number | no | Corridor half-width in metres. Default 60; values above 500 are clamped to 500; zero, negative or non-numeric is a `400`. |

### Example

```sh
curl -fsS -X POST "$BASE/v1/incidents/along" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "shape": "}~ycbBhavgN...", "buffer_m": 60 }'
```

### Response

```json
{
  "incidents": [
    { "id": "7-1784655805-...-close", "arc_m": 0.0,
      "from_arc_m": 0.0, "to_arc_m": 1326.1, "offset_m": 0.0,
      "side": "unknown", "cause": "roadOrCarriagewayOrLaneManagement: laneClosures",
      "severity": "lane_restriction", "closure_type": "laneClosures",
      "road": "M1", "lanes_restricted": 1, "lanes_operational": 3,
      "status": "active", "planned": false, "probable": false,
      "overall_start": "2026-07-26T10:00:00Z", "overall_end": "2026-07-27T10:00:00Z",
      "source": "uk-national-highways" }
  ],
  "sources": [
    { "source_id": "uk-national-highways",
      "network": "England strategic road network (motorways and trunk roads managed by National Highways)",
      "coverage_note": "National Highways only. Local roads, and the separate Scotland/Wales trunk-road networks, are not covered.",
      "licence": "OGL-UK-3.0" }
  ],
  "attribution": ["Contains public sector information licensed under the Open Government Licence v3.0."]
}
```

- `incidents` is ordered by `arc_m`. `from_arc_m`/`to_arc_m` bound the
  extent of the closure's geometry that actually fell inside the
  corridor; for a closure spanning a long stretch of motorway this can
  be several kilometres; for one that only clips the corridor briefly,
  `from_arc_m` and `to_arc_m` sit close together.
- `severity` (`closure` | `lane_restriction` | `other`) is derived only
  from the feed's own closure-type and lane-count fields, never a
  guess when those are absent.
- `side` (`left` | `right` | `unknown`) is geometric: which side of the
  route's own local bearing the incident sits on. The feed carries no
  carriageway-direction field, so this is not the same question as
  "does this affect my direction of travel".
- **`sources` is always present, even with zero matching incidents.**
  An empty `incidents` array means "nothing on the covered network near
  this route", never "the road is clear". Read `coverage_note` before
  trusting silence: National Highways covers the English strategic road
  network only, not local roads and not the separate Scotland/Wales
  networks.

### Availability and billing

Bills as one **Standard** call, never Premium, the same posture as
`/v1/cameras/along`. On deployments without incident data the endpoint
answers `501` (`urn:sn-gateway:problem:incidents-not-enabled`);
treat that as "feature not available here", not an error. Self-hosters
enable it by pointing `SN_INCIDENTS_DIR` at a directory of per-source
sidecar files (see [`docs/API.md`](https://github.com/Mapmapai/mapmap/blob/main/docs/API.md)
for the sidecar shape). Licence: OGL v3.0 (UK Open Government Licence);
the `attribution` array carries the required notice verbatim.

## Weather along a route (`POST /v1/weather/along`)

Samples a route and returns forecast weather for each point **aligned to
when you actually get there**, not to now. A two-hour route departing at
noon reads the forecast for the departure hour at its start and the
"two hours from now" hour at its end; that time alignment is the whole
point: "you hit the snow band near Shap at 14:00", not "it is snowing
somewhere on this route right now".

### Parameters

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `shape` | string | yes | The route geometry as a polyline6 string. |
| `depart_at` | string | no | RFC 3339 departure time. Defaults to now. |
| `duration_s` | number | no | Total route duration in seconds, used to compute each sample's ETA. Omit for a generic average-speed estimate (flagged `duration_source: "estimated_default_speed"` in the response); pass the real value from `/route` for accurate alignment. |
| `sample_interval_m` | number | no | Sampling interval in metres. Default 25,000; clamped 5,000–100,000; capped at 40 samples total regardless of route length. |

### Example

```sh
curl -fsS -X POST "$BASE/v1/weather/along" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "shape": "}~ycbBhavgN...", "depart_at": "2026-07-26T12:00:00Z", "duration_s": 7200 }'
```

### Response

```json
{
  "samples": [
    { "lat": 54.4, "lon": -2.7, "arc_m": 180000.0,
      "eta": "2026-07-26T14:00:00Z", "forecast_hour": "2026-07-26T14:00",
      "available": true, "temperature_c": -1.2, "precipitation_mm": 0.6,
      "rain_mm": 0.0, "snowfall_cm": 0.8, "precipitation_type": "snow",
      "precipitation_probability_pct": 80.0,
      "wind_speed_kph": 32.0, "wind_gusts_kph": 58.0,
      "visibility_m": 3200.0, "weather_code": 71, "severe": false }
  ],
  "depart_at": "2026-07-26T12:00:00Z", "duration_s": 7200.0,
  "duration_source": "provided", "sample_interval_m": 25000.0,
  "attribution": "Weather data by Open-Meteo.com (CC BY 4.0)"
}
```

- `wind_gusts_kph` is the figure that matters for high-sided vehicles.
- `severe` is derived only from the source's WMO weather code
  (thunderstorm and heavy-precipitation codes), never a proprietary
  severe-weather product, and omitted (not `false`) when the sample
  itself is unavailable.
- A sample whose upstream fetch failed, or whose ETA falls beyond the
  source's forecast horizon, comes back `"available": false` with an
  `unavailable_reason`; the request never fails outright because one
  sample couldn't be resolved.

### Availability, billing and licence

Bills as one **Standard** call regardless of sample count. 501
(`urn:sn-gateway:problem:weather-not-enabled`) unless the operator sets
`SN_OPEN_METEO_URL`. **Read this before enabling commercially:**
Open-Meteo's free, keyless endpoint is non-commercial use only; a
commercial deployment needs an Open-Meteo paid plan or a self-hosted
instance (Open-Meteo is open source, AGPLv3). See
[`THIRD-PARTY-NOTICES.md`](https://github.com/Mapmapai/mapmap/blob/main/THIRD-PARTY-NOTICES.md)
for the full licence position. The weather data itself is CC BY 4.0;
every response carrying data includes an `attribution` string.

## Errors

Errors are `application/problem+json` (see
[conventions](/docs/conventions) for the envelope and the 402-vs-429
distinction). All eight endpoints share the same table:

| Status | `type` URN | Meaning | Retry? |
| --- | --- | --- | --- |
| 400 | `urn:sn-gateway:problem:bad-request` | Malformed JSON; empty `locations`, `contours`, `sources` or `targets`; a contour with neither `time` nor `distance`; a trace request with no trace; an along-route request with neither/both of `route` and `shape`, or an out-of-range `corridor_m`/`max_detours`; a cameras or incidents request with an undecodable `shape` or a non-positive `buffer_m`; a weather request with a non-RFC-3339 `depart_at` or a non-positive `duration_s`/`sample_interval_m`. | No; fix the request. |
| 404 | `urn:sn-gateway:problem:not-found` | `/route/along` only: customer places are not configured on this deployment. | No. |
| 501 | `urn:sn-gateway:problem:cameras-not-enabled` / `…:incidents-not-enabled` / `…:weather-not-enabled` | The corresponding endpoint's data source is not configured on this deployment. | No. Feature-detect and disable that feature. |
| 401 | `urn:sn-gateway:problem:unauthorized` | Missing or invalid API key. | No. |
| 429 | `urn:sn-gateway:problem:quota-exceeded` or `…:rate-limited` | Monthly quota or rate limit; `rate-limited` bodies carry `retry_after` (seconds). | After `retry_after`. |
| 502 | `urn:sn-gateway:problem:upstream-error` | The routing engine failed or returned garbage. | Yes; transient. |

Note that `/v1/weather/along` never returns a 502 for a per-sample
upstream failure: it degrades that sample to
`"available": false` inside a normal `200` response instead (see above).

## Next steps

- [Route optimisation](/docs/optimisation): the solver that consumes these matrices, with the same truck/ADR constraints
- [API reference](/docs/api-reference): the full endpoint surface, including `POST /route` (whose response `/trace_route` shares) and the `/places` API that feeds along-route search
- [Conventions](/docs/conventions): base URL, auth, error envelope, ADR tunnel codes
- [MCP server](/docs/mcp): the `matrix` and `search_along_route` tools expose these capabilities to agents
