POST /matrix bills a call per started block of 25 elements

# POST /matrix bills a call per started block of 25 elements

`POST /matrix` returns the road travel time and distance between every source and
every target, as two row-major arrays: `durations` in seconds, `distances` in
metres, `null` where a pair is unreachable. It bills one call per started block of
25 elements, where an element is one `sources × targets` pair, capped at 10,000
elements per request. A 12 by 40 dispatch matrix is 480 elements, so 20 blocks, so
20 billable calls: 1p at the standard rate, 20p with truck costing. The same
computation runs inside `POST /optimise`, which is why truck dimensions and ADR
constraints shape a fleet plan rather than getting checked after it.

## What does POST /matrix return?

Two arrays, and nothing else. `sources` are the rows, `targets` are the columns,
and `durations[i][j]` is the time from `sources[i]` to `targets[j]`. Locations are
`{"lat": …, "lon": …}` objects with named keys, so there is no coordinate order to
get wrong.

```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 } }
}'
```

The response below is not truncated. That is the entire shape:

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

One depot and two targets gives one row of two. Because that request carried
truck costing with a 4 m height and a 40 t weight, the durations are times a
40-tonne lorry can achieve on roads it is allowed to use, not car times with a
margin added.

| Field | Required | Notes |
|---|---|---|
| `sources` | yes | Row locations (origins), array of `{lat, lon}` |
| `targets` | yes | Column locations (destinations) |
| `costing` | yes | `auto`, `truck`, `bus`, `motor_scooter`, `motorcycle`, `bicycle` or `pedestrian` |
| `costing_options` | no | Passed to the engine verbatim, keyed by costing name |
| `units` | no | Accepted, then ignored: distances always come back in metres |
| `id` | no | Opaque identifier, echoed back |

Unreachable pairs come back as an honest `null`. We do not substitute a large
number and let you discover the difference in production.

## How much does a matrix request cost?

Multiply blocks by your class rate. Elements are `sources × targets`. Blocks are
elements divided by 25 and rounded up, with a floor of one block. The class is
Standard at 0.05p normally, and Premium at 1p when the body carries
`"costing": "truck"`, selected automatically per request.

| Matrix | Elements | Billed calls | Standard | Premium (truck) |
|---|---|---|---|---|
| 1 × 1 | 1 | 1 | 0.05p | 1p |
| 5 × 5 | 25 | 1 | 0.05p | 1p |
| 5 × 6 | 30 | 2 | 0.1p | 2p |
| 12 × 40 | 480 | 20 | 1p | 20p |
| 100 × 100 | 10,000 | 400 | 20p | £4 |

Read the 5 × 6 row twice, because it is the one that surprises people. A
30-element matrix is one full block plus one started block, and a started block
bills as a whole block. Adding a single target to a 5 × 5 matrix doubles its price. The
effect fades at scale (480 elements rounds 19.2 blocks up to 20, an overshoot of
about 4%), but it is real at the bottom end and we would rather you read it here
than infer it from an invoice.

Those prices are the entry band. Bands are marginal and selected by month-to-date
usage, and every block counts as one call against them, so a single 100 × 100
matrix advances you through the bands exactly as far as 400 route requests would.
Standard falls to 0.03p beyond 1M calls a month and 0.02p beyond 10M. Premium
falls to 0.7p and 0.4p across the same bands. Prices reconcile with
[/pricing.json](/pricing), which wins if this page ever drifts.

On the free tier, the arithmetic runs through the premium weight. The 50,000
included monthly calls are denominated in standard calls, and one premium call
draws 20 of them. So the 12 by 40 truck matrix above is 20 premium calls, which
draws 400 included calls, and the free tier absorbs 125 of those requests a month.
The same matrix with `"costing": "auto"` draws 20 included calls, and the free
tier absorbs 2,500 of them.

**Billing precision.** The gateway meters an internal millipence ledger at 1p =
1,000 millipence, so fractional pence bill exactly rather than rounding up per
call. A one-block standard matrix debits 50 millipence. `GET /v1/keys/self`
returns `credits_pence` (floored) and `credits_millipence` (full precision), which
is why a single small matrix can move your balance without moving the pence
figure. Pay-as-you-go is prepaid credit, and the monthly plans bill a flat fee
with overage falling back to that same credit, so nothing is ever post-paid. With
no credit you get a `402` or a `429`, never a surprise invoice.

## What happens if you ask for more than 10,000 elements?

A `400`, raised before any metering or debit, so an oversized matrix costs you
nothing. The gateway multiplies `sources` by `targets` in the auth layer and
rejects the request there:

```
matrix too large: 150 sources × 150 targets = 22500 elements exceeds the
10000-element limit; split the request
```

The 10,000-element ceiling is a 100 × 100 matrix, and 400 billed calls is the most
a single `/matrix` request can ever cost. Split larger problems by source block.

## Why does /optimise compute its matrix through our engine?

Because a solver left to do its own routing would plan legs a lorry cannot drive.
`POST /optimise` (alias `/optimize`) never delegates routing to the solver sidecar.
The gateway computes the full duration and distance matrix through its own engine
first, with your `costing`, `costing_options` and `adr` profile applied, then hands
the solver explicit matrices. The optimisation therefore respects exactly the same
constraints as `/route` and `/matrix`: a bridge too low for your trailer is not a
cheap edge the solver can be tempted by, because the matrix cell that crosses it
already reflects the detour.

The billing follows a different rule, and it favours you. One optimisation request
bills a flat 10 calls of its class regardless of problem size, and the internal
matrix is not billed separately. That is 10p for a truck plan and 0.5p for a car
plan, whether the problem has five stops or the fair-use ceiling of 200 unique
locations. A 200-location plan implies a 200 × 200 internal matrix, which is 40,000
elements, four times more than you may request directly from `/matrix`, still for
a flat 10 calls. If you want a plan, ask for a plan. Call `/matrix` directly when
you want the raw numbers for your own solver or your own ranking logic.

Two differences worth holding in mind. Unreachable pairs are `null` on `/matrix`
and a large sentinel cost inside `/optimise`, so an absurd duration in a plan means
a stop is probably unreachable rather than distant. And `/optimise` is enabled per
deployment: with no solver sidecar configured it returns `503` with problem type
`urn:sn-gateway:problem:optimisation-not-enabled`. We wrote the solver up in full
in [route optimisation with truck and ADR constraints](/news/route-optimisation)
and in the [optimisation guide](/docs/optimisation).

## What POST /matrix does not do

**It applies no ADR restrictions, and it charges you premium for asking.** This is
the trap and it is worth the bold type. The `adr` extension is honoured only on
`POST /route` and `POST /optimise`. Send a top-level `adr` object to `/matrix` and
it is ignored, but the pricing classifier still sees it and bills the whole request
as Premium, at 20 times the standard rate, for a matrix with no ADR enforcement in
it. Truck dimensions via `costing_options.truck` do shape a matrix. Tunnel-category
compliance does not. Do not build a dangerous-goods step on this endpoint.

**It returns no geometry.** A matrix is numbers, not lines. Fetch each leg from
`POST /route` when you need something to draw.

**`units` is accepted and then ignored.** The gateway normalises response distances
to metres whatever you pass. We would rather one unit in the response than a field
that silently changes what your integers mean.

## Try it

The [analysis guide](/docs/analysis) carries the full parameter and error tables
for `/matrix` alongside the other three analysis endpoints, and
[isochrones, priced per contour](/news/isochrones) covers the reachability side of
the same family. A key comes from one card-free call to `POST /v1/keys` and works
immediately. Agents can reach the same capability as the `matrix` tool on our MCP
server.

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