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.
Request
Section titled “Request”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
| Field | What |
|---|---|
id | Unique name, used in the answer |
start | Where the vehicle starts: a place as in POST /v1/route ({"station": code}, {"lat", "lon"} or {"osm_node"}) |
end | Where it must end; without it, the route ends at its last stop |
capacity | What 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_reversal | As 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 inPOST /v1/route.objective:duration(default: least total running time) ordistance(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).
Response
Section titled “Response”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.stepsarestart, the stops (job,pickup,deliverywith the job’s or shipment’sid) andendif the vehicle has one;locationindexeslocations(the distinct places in order of first use).arrival,departureare times on your timeline,waiting_sthe wait for the window,loadwhat is on board after the step, anddistance_m/duration_sthe 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 areason:
| Reason | Meaning |
|---|---|
unreachable | No vehicle has a route there (and on to the delivery and its end) for its constraints |
capacity | Too much for every vehicle that reaches it, or no route has room left for it |
time_window | No vehicle gets there within the windows, or it fits into no route’s schedule |
summary.costis the objective: seconds of running time, or metres withobjective: distance.solver: the cost after construction and at the end, ruin and recreate rounds (iterations), improving moves, solver time.timed_out: truemeans 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.
Larger problems: jobs
Section titled “Larger problems: jobs”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.
How it works
Section titled “How it works”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).
Size limits
Section titled “Size limits”| Plan | Vehicles | Stops (jobs + 2 × shipments) | Matrix elements |
|---|---|---|---|
| Free | up to 5 | up to 50 | up to 10,000 |
| Invoiced | up to 100 | up to 1,000 | up 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).
Metering
Section titled “Metering”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.