Skip to main content
A heating and electrical contractor in Ghent plans one working day: eight jobs for three technicians who start and end at the same workshop. Some jobs need a gas-certified or electrically qualified technician, two need a boiler from the workshop, two visits to one customer must be done by the same person, and one maintenance visit is optional. Everyone takes a 30-minute lunch break that starts between 12:00 and 13:00. This page solves that day with POST /v3/routing/solve, reads the response, and then reuses the same data for two follow-ups: scoring a manual change with /v3/routing/evaluate and placing a new job with /v3/routing/suggest. The solve and evaluate responses are real output for the requests shown. Yours will differ in the travel figures, which depend on live road data.

The scenario

All times in the request are local time on 4 November 2026, written with the +01:00 offset. How each requirement maps to the request:
  • Time windows. time_windows on each job. They are hard: a job that cannot be reached inside its window is left unassigned.
  • Skills. skills on a job lists what it requires, skills on a vehicle lists what the technician provides. A job goes only to a vehicle that provides every skill it names. See Skills.
  • Boilers. delivery: { "boilers": 1 } on the two install jobs is stock loaded at the workshop and dropped at the job. It counts against the van’s capacity: { "boilers": 1 }, so the two installs cannot share a van. See Capacity.
  • Same technician. One same_resource relation names the two Deinze visits. See Relations.
  • Lunch. A floating break of 1,800 seconds on each shift, with one window that bounds when the break may start. See Driver breaks.
  • Optional visit. mandatory: false with drop_fee: 30 on the Bruges check. It is served only when serving it costs less than 30. See Priority and optional tasks.
  • Workshop. One depots[] entry, referenced by id from each shift’s start and end.
  • Cost. objective.costs prices a technician’s time at 40 per hour of route and driving at 0.30 per kilometer, labeled EUR.

Solve the day

options.time_limit_s gives the search two seconds. Add ?validate_only=true to the URL to check the body first without solving.

Read the response

The response below is the real output for that request, with one thing trimmed: each stop’s snapped_location (the coordinate snapped to the road network) is removed to keep it short. Response times are UTC, so 07:00:00Z is 08:00 local time.

Summary

  • status is partial: seven of the eight jobs are on a route and one is in unassigned[]. The call still returns HTTP 200.
  • vehicles_used is 2. routes[] has an entry for anna and one for bram. chloe serves nothing, and a vehicle shift with no stops gets no routes[] entry. This request puts no price on using a vehicle; see Objective function for how to add one.
  • total_distance_m and total_duration_s are sums over the routes. total_duration_s is driving time only: 5,690 s + 5,651 s = 11,341 s.

Routes and stops

Each route names its vehicle and shift by the ids you sent, and lists its stops in visiting order between a start and an end stop. Those two stops carry depot: "workshop" because the shift references the depot by id. Anna’s day, in local time with the seconds dropped: What to take from it:
  • Skills. Every gas job is on anna or bram, and both electrical jobs are on anna, who has both skills.
  • Capacity. Each van carries one boiler, so the two installs are on different routes. load_peak is the highest load on the route: { "boilers": 1 } on both.
  • The relation. boiler-install-deinze and boiler-commission-deinze are both on anna, as the same_resource relation requires.
  • Time windows. arrival is when the vehicle reaches the stop. The commissioning visit starts at 14:00, the moment its window opens.
  • Breaks. A break stop has no id and no location. Its arrival and departure are the start and end of the break. It is listed directly before the next stop on the route, and in this response each break starts after the vehicle has reached that stop, so a break’s arrival can be later than the arrival of the stop listed after it. Anna reaches Deinze at 11:31, takes her break from 12:00 to 12:30, and then does the 90-minute install. That is why the install’s departure minus its arrival is longer than its service_s: the interval also holds the break and the time before it. Bram has no job left when his break window opens at 12:00. A break whose window opens before the shift’s to is still taken on a route that is in use, so his lands at the end of the route: he reaches the workshop at 12:04 and the end stop has arrival 12:04 and departure 12:34.
  • Durations. duration_s is driving time only. shift_duration_s is the route’s span from start to finish, including service, waiting and the break: 28,110 s for anna (08:00 to 15:48) and 16,451 s for bram (08:00 to 12:34).
  • plan. The same routes in the shape a request accepts: task stops only, without start, end and break stops. The two follow-ups below send it back.

The unassigned job

Read category first. priced_out means the job fits, it has a drop_fee, and serving it would cost at least that fee, so the solver paid the fee instead. The 30 shows up in score.components.drop_fees, and score.unserved_priority stays 0 because the job was not must-serve. The reason code for a priced_out job is SOLVER_LIMIT, and its message mentions the time budget. That code is shared with the search_limit category, so branch on category and not on the code: a longer time_limit_s would leave this job out again. relaxations is empty for this job. Errors lists every category and reason code.

Score

score.cost is the sum of score.components, in the currency you labeled: Every component is always present, so the ones this request does not use are 0.0. violations is always 0 on a solve response. See Objective function for each component.

Score a manual change with evaluate

A dispatcher decides the Bruges check has to happen today and gives it to bram, who is free after lunch. Before committing that, send the edited plan to /v3/routing/evaluate. The problem is unchanged. The plan is the solve response’s plan with one stop appended to Bram’s route:
Evaluate takes problem and plan and no options.
The real response, trimmed to summary, Bram’s route and unassigned. Anna’s route comes back exactly as in the solve response, and plan echoes the plan you sent. snapped_location is removed as before.
How to read it:
  • status is feasible: the edited plan breaks no hard rule. On evaluate the value is feasible or infeasible, and a broken rule is listed as a violations[] entry on the stop or route it concerns. There is no iterations field, because nothing is searched.
  • The new stop. Bram reaches Bruges at 12:08 local time and takes his lunch break there. The job’s window opens at 15:00, so wait_s is 8,464 s, and he leaves at 15:45. His route now ends at 16:22 instead of 12:34.
  • unassigned is empty: every job of problem is in the plan.
The change has a price. cost rises from 583.48241 to 721.54001, which is 138.0576 more: Serving the visit this way costs 168.0576 against a fee of 30, which is why the solve left it out. If the visit must always be planned, remove mandatory: false and drop_fee from the job, or raise the fee to what a missed visit costs you. See Evaluate for the violation types.

Place a new job with suggest

At 11:00 a customer in Destelbergen reports a leaking radiator and can be visited between 13:00 and 17:00. The routes are already under way, so ask where the new job fits without rearranging them. A suggest request has three parts:
  • problem: the same problem with the new job added to jobs.
  • plan: the plan you are keeping. Here it is the solve response’s plan, unchanged.
  • options.tasks: the ids to place.
The job to add:
And the options:
The response has one suggestions[] entry for radiator-leak-destelbergen with up to three options, cheapest first. Each option names the vehicle, the position among that route’s task stops, the neighboring stops (after_job_id, before_job_id), the arrival at the new stop, and what the placement adds: extra_cost (an integer in units of 1/100,000 of your currency), extra_distance_m and extra_duration_s. The job requires gas, so no option names chloe. annual-check-brugge is still in problem but in neither plan nor options.tasks, so it stays unassigned and gets no suggestions. Suggest changes nothing. To act on an option:
1

Insert the stop

Add { "type": "job", "id": "radiator-leak-destelbergen" } to the plan.routes[] entry of the option’s vehicle, at index position of its stops.
2

Score the result

Send the edited plan to /v3/routing/evaluate, as in the previous section, to get the full timeline and cost.
To mark stops that are already done or under way, set locked_count on a route in plan: no option lands before the locked stops. This page does not show a suggest response; Suggest has an annotated one and the full list of option fields.

Next steps

The V3 API model

Every request and response field.

Constraints

The rules used here, and the ones this example leaves out.

Evaluate

The plan shape and every violation type.

Suggest

Reading and applying insertion options.