Skip to content

Distance matrix

POST /v1/matrix answers “how far is it, by rail, from each of these places to each of those, for this train?” — the input for freight pricing and network planning. Every cell is the route POST /v1/route would find for that pair, with the same vehicle rules (electrification, axle load, gauging, gauge, train protection, maximum speed, reversals, avoided lines and areas). It needs a key with the route scope.

Terminal window
curl -X POST -H "Authorization: Bearer $RAIL_API_KEY" -H "Content-Type: application/json" \
https://api.routing.gleisklar.de/v1/matrix -d '{
"origins": [{ "station": "AH" }, { "station": "AA" }],
"destinations": [{ "station": "AK" }, { "station": "AL" }, { "lat": 54.7741, "lon": 9.4367 }],
"profile": "freight-electric-de"
}'
  • origins, destinations: places as in POST /v1/route ({"station": code}, {"lat", "lon"} or {"osm_node"}); at most 1,000 each. Without destinations, the origins are also the destinations.
  • profile, vehicle, avoid, reversal_min, no_reversal: as in POST /v1/route.
  • async: true: run as a job even if the matrix is small (see below).
{
"origins": [{ "name": "Hamburg Hbf", "kind": "station", "ds100": "AH", "lon": 10.0065, "lat": 53.5528,
"snapped": { "lon": 10.00667, "lat": 53.55297, "distance_m": 21.4 } }, …],
"destinations": [ … ],
"durations_s": [[4278, 3174, null], [3960, 2826, null]],
"distances_m": [[98412, 70980, null], [92118, 64790, null]],
"status": [["ok", "ok", "blocked"], ["ok", "ok", "blocked"]],
"summary": { "ok": 4, "blocked": 2, "not_connected": 0, "same_location": 0 },
"elements": 6,
"profile": "freight-electric-de",
"vehicle": { … },
"data_version": "20261104-0200",
"warnings": []
}

(Illustrative values.) [i][j] is origin i to destination j. Origins and destinations come back as resolved, with snapped where they meet track the vehicle may use (absent: none within 1.5 km). Without a route the duration and distance are null and status says why:

StatusMeaning
okA route for this vehicle
blockedA route exists, but not for this vehicle (its constraints, what is avoided, or reversing with no_reversal)
not_connectedNo route even ignoring the vehicle
same_locationOrigin and destination are the same place on the network: 0 s, 0 m

A blocked cell carries no reason: route the pair with POST /v1/route, whose no_route names the constraint and every blocking stretch.

Durations are the routing cost, in seconds: the time at the line speed capped at the vehicle’s max_speed_kmh, plus reversal_min per reversal, with service track (sidings, yards) counted three times — the time the route search minimises. duration_min of POST /v1/route is the same except that it counts service track once, so a cell whose route uses service track is a little longer than that route’s duration_min. They have no acceleration and no braking and are not the route’s running time estimate (see travel time estimates): computing that for up to a million cells would take milliseconds each. Distances are metres along that route.

Elements are origins × destinations. Per request:

PlanElements
Freeup to 10,000
Invoicedup to 1,000,000

Larger requests are refused with 413 (too-large). Up to 250,000 elements the matrix is the answer (200). A larger matrix, or any with "async": true, is answered with 202 and a job:

{ "job_id": "mx_4u3k…", "status": "queued", "progress": 0, "elements": 400000,
"poll_url": "/v1/matrix/jobs/mx_4u3k…", "data_version": "20261104-0200",
"created_at": "2026-11-04T12:00:00Z" }

Poll GET /v1/matrix/jobs/{id} (the poll_url, also in the Location header) every few seconds: status goes queued → running (progress: share done, 0–1) → done, with the matrix in result (or failed with error). Sending the same request again while its job exists returns that job (200) instead of computing it twice. See async jobs for how long jobs are kept and how many can run.

The JavaScript client polls for you:

const m = await api.matrixResult(
{ origins, destinations, profile: "freight-electric-de" },
{ intervalMs: 2000 },
);

A matrix is metered per element (elements), when it is answered or when the job is accepted; a job that fails later is not refunded. Billing weighs an element as 0.1 requests (10 elements = 1 billable request; a 50 × 50 matrix bills as 250). Each poll counts as one request. Failed requests (400, 413, 429) count nothing.