Skip to content

Route optimization

POST /v1/optimize answers “we have these locomotives and these wagons to collect and deliver: which train goes where, in which order, and when?”. It plans routes for several vehicles over many stops — single stops (jobs) and pickup-and-delivery pairs (shipments) — respecting each vehicle’s capacity and working hours, the stops’ time windows and the vehicle rules of POST /v1/route (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/optimize -d '{
"vehicles": [
{ "id": "loco-1", "start": { "station": "AH" }, "end": { "station": "AH" }, "capacity": [30],
"time_window": [21600, 79200], "profile": "freight-electric-de" },
{ "id": "shunter-1", "start": { "station": "AA" }, "capacity": [12],
"vehicle": { "electrification": "none" } }
],
"jobs": [
{ "id": "kiel", "location": { "station": "AK" }, "service_s": 1800, "delivery": [10],
"time_windows": [[28800, 43200]] },
{ "id": "neumuenster", "location": { "station": "AN" }, "service_s": 1200, "pickup": [6] }
],
"shipments": [
{ "id": "wagons-luebeck", "amount": [8],
"pickup": { "location": { "station": "AL" }, "service_s": 2700 },
"delivery": { "location": { "station": "AA" }, "service_s": 2700 } }
]
}'

Vehicles

FieldWhat
idUnique name, used in the answer
startWhere the vehicle starts: a place as in POST /v1/route ({"station": code}, {"lat", "lon"} or {"osm_node"})
endWhere it must end; without it, the route ends at its last stop
capacityWhat it carries at most, as a vector: e.g. [wagons] or [wagons, tonnes]. Every vehicle needs the same dimensions (up to 5)
time_window[earliest start, latest end]: it leaves the start from the first and must be done (at its end, or after its last stop) by the second
profile, vehicle, reversal_min, no_reversalAs in POST /v1/route

Jobs are single stops: id, location, service_s (time at the stop: shunting, loading), delivery (an amount loaded at the vehicle’s start and unloaded here), pickup (loaded here and carried to the end) and time_windows.

Shipments carry an amount from a pickup to a delivery stop (each with location, service_s, time_windows) on the same vehicle, pickup first.

Times are whole seconds on a timeline of your choice — seconds since midnight of the planning day, Unix time, … — the same for every window. A stop’s service has to start within one of its time_windows (up to 10, [start, end]); a vehicle that is early waits. Without windows, any time goes.

Options:

  • avoid: lines and areas no vehicle may use, as in POST /v1/route.
  • objective: duration (default: least total running time) or distance (least total distance). Either way, serving as many jobs and shipments as possible comes first; waiting time is not minimised.
  • time_limit_s: time for the solver, default 5, at most 60 seconds. Small problems finish much earlier.
  • async: true: run as a job even if the problem is small (see below).

Up to 100 stops (jobs + 2 × shipments) and a time_limit_s of at most 10, the plan is the answer (200):

{
"objective": "duration",
"locations": [{ "name": "Hamburg Hbf", "kind": "station", "ds100": "AH", "snapped": { … }, … }, …],
"routes": [
{
"vehicle": "loco-1",
"steps": [
{ "type": "start", "location": 0, "arrival": 25080, "departure": 25080, "waiting_s": 0,
"service_s": 0, "load": [10], "distance_m": 0, "duration_s": 0 },
{ "type": "job", "id": "kiel", "location": 2, "arrival": 28800, "waiting_s": 0,
"service_s": 1800, "departure": 30600, "load": [0], "distance_m": 98412, "duration_s": 3720 },
…,
{ "type": "end", "location": 0, "arrival": 41220, … }
],
"distance_m": 211730, "duration_s": 9840, "waiting_s": 0, "service_s": 3000,
"start_time": 25080, "end_time": 41220
}
],
"unassigned": [
{ "type": "job", "id": "flensburg", "reason": "time_window",
"message": "no vehicle that can reach and carry it gets there within its time windows and the vehicle's" }
],
"summary": { "cost": 15720, "routes": 2, "assigned": 3, "unassigned": 1, "distance_m": 302540,
"duration_s": 15720, "waiting_s": 600, "service_s": 8400 },
"solver": { "construction_cost": 16980, "cost": 15720, "iterations": 200, "moves": 14, "ms": 38,
"timed_out": false },
"elements": 72, "data_version": "20261104-0200"
}

(Illustrative values.)

  • routes: one per vehicle used, in request order. steps are start, the stops (job, pickup, delivery with the job’s or shipment’s id) and end if the vehicle has one; location indexes locations (the distinct places in order of first use). arrival, departure are times on your timeline, waiting_s the wait for the window, load what is on board after the step, and distance_m / duration_s the leg from the previous step. A vehicle leaves later than its earliest start rather than wait at its first stop.
  • unassigned: what couldn’t be planned, with a reason:
ReasonMeaning
unreachableNo vehicle has a route there (and on to the delivery and its end) for its constraints
capacityToo much for every vehicle that reaches it, or no route has room left for it
time_windowNo vehicle gets there within the windows, or it fits into no route’s schedule
  • summary.cost is the objective: seconds of running time, or metres with objective: distance.
  • solver: the cost after construction and at the end, ruin and recreate rounds (iterations), improving moves, solver time. timed_out: true means the time limit stopped the search; the result is still a valid plan.

Travel times are the routing time of the distance matrix for each vehicle: line speeds capped at its maximum speed, reversals at reversal_min, service track counted three times; no acceleration or braking, no timetable or path conflicts. Plan with margins.

More than 100 stops, a time_limit_s above 10, or "async": true: the answer is 202 with a job, as for a matrix:

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

Poll GET /v1/optimize/jobs/{id} (the poll_url, also in the Location header) every second or two: status goes queued → running (progress: share done) → done with the plan in result (or failed with error). The same request again returns that job. See async jobs for how long jobs are kept and how many can run. With the SDK, api.optimizeResult(body) returns the plan either way, polling a job until it is done.

The request resolves every location, then computes the travel time and distance matrix between all distinct locations, once per distinct vehicle profile (vehicles with the same constraints share one). Pairs without a route for a vehicle can’t be driven by it. The solver then inserts the stops one by one at their cheapest feasible place (tightest time windows first) and improves the plan by local search — moving stops and short runs of stops within and between routes, swapping stops, reversing stretches, exchanging route tails — and by repeatedly removing a few neighbouring stops and reinserting them.

The solver is deterministic: the same request gives the same plan, unless the time limit cuts the search short (solver.timed_out).

PlanVehiclesStops (jobs + 2 × shipments)Matrix elements
Freeup to 5up to 50up to 10,000
Invoicedup to 100up to 1,000up to 1,000,000

Matrix elements are distinct locations² × distinct vehicle profiles: 20 locations and two kinds of locomotive make 800. Larger requests are refused with 413 (too-large), invalid ones with 400 (the problem’s errors point at the field, e.g. /jobs/3/time_windows/0).

An optimization is metered like a matrix: per matrix element it computes (elements), when it is answered or 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; 20 locations for one vehicle profile bill as 40). Each poll counts as one request. Failed requests (400, 413, 429) count nothing.