Skip to main content
V3 is in beta. General availability (GA) is planned for January 2027.
A path is a reading order for one mission. Each step names the page to read, the call to make, and the mistake it tends to produce. The “Skip” line at the end of each path names the pages you do not need. On every path (Requests): the base URL is https://api.solvice.io, the key goes in Authorization with no Bearer prefix, and a field the schema does not define is 400 with item code unknown_field.

Add V3 to an existing application

Use this path when the application exists and V3 should plan its routes: a synchronous POST /v3/routing/solve first, then evaluate and suggest.
  1. Read Authentication and Requests. Write coordinates as [longitude, latitude]. Pitfall: a 401 comes from the gateway with an empty body or one that is not the error body; branch on the HTTP status before you parse JSON.
  2. Read Your first solve. Send a minimal problem with jobs[] and vehicles[] to POST /v3/routing/solve with options.time_limit_s of a few seconds. Pitfall: a timestamp without an offset returns 400 with a pointer to the field; write +02:00 or Z on every from, to and window.
  3. Read A request at a glance, then Jobs and Vehicles and shifts. One jobs[] entry per visit, one shipments[] entry per pickup-then-delivery pair, one shifts[] entry per vehicle per working day, each with an id. Pitfall: a field name carried over from another routing API returns 400 with item code unknown_field; fix the field at the pointer using the reference’s name.
  4. Read Capacity, Time windows and Skills. Turn on only the rules your data asserts. Pitfall: a misspelt capacity dimension silently gives the vehicle capacity 0 in it, and the job comes back in unassigned[] with capacity; match names exactly, and omit capacity rather than send {} to mean “no limit”.
  5. Read Objective and the V3 defaults. Pitfall: an omitted objective silently prices every route at 35 per route hour and 0.20 per kilometre; set objective.costs to your rates before comparing plans.
  6. In CI, send every request to POST /v3/routing/validate/solve (Dry run): 200 {"valid": true, "warnings": []} or the real call’s 400, not billed. Pitfall: validate checks problem and options only; a bad warm-start plan and coordinates spanning more than one region are reported by the real call alone.
  7. Read Response and Unassigned tasks. Pitfall: a plan that leaves tasks out is still HTTP 200, so a client that checks the status alone silently ships an incomplete plan; check summary.tasks.unassigned_required, then unassigned[].category and reasons[].constraint.
  8. Read Solve time and Client timeout. Pitfall: time_limit_s bounds the search only, so an HTTP timeout equal to it silently aborts requests about to succeed; set it to the budget plus the measured overhead. A budget above 60 returns 400 with item code limit_exceeded at /options/time_limit_s; use an async run instead.
  9. Read Evaluate and Violations. Send problem plus a returned plan to POST /v3/routing/evaluate to score a hand-edited plan. Pitfall: evaluate takes no options; sending one returns 400 with item code unknown_field. A plan with violations is 200: read summary.score.violation_count.
  10. Read The request and Applying a suggestion. Add the new task to problem, leave it out of plan, and call POST /v3/routing/suggest. Pitfall: an empty insertions list is 200 with a reason.code, and only infeasible is final; raise options.time_limit_s for time_limit and options.max_insertions for candidates_exhausted.
  11. Read What your client must do. Pitfall: a generated client with closed enums fails on a new category, constraint or error code; accept unknown values and never parse message.
Skip: Migration from V2 and the V2 tab, Async runs unless a problem has more than 400 tasks or needs more than 60 seconds, and the Routing Concepts pages beyond the two sections above. Return to Constraints one rule at a time.

Migrate from V2

Use this path when you call /v2/vrp/* today. How to port a request is the procedure; this path adds the pitfalls.
  1. Read Overview and Endpoints and lifecycle. POST /v2/vrp/sync/solve becomes POST /v3/routing/solve; the job lifecycle (POST /v2/vrp/solve, GET /v2/vrp/jobs/{id}) becomes POST /v3/routing/runs and GET /v3/routing/runs/{id}. Pitfall: the V2 hook has no counterpart; a body with callback returns 400 with item code not_supported at /callback; poll the run.
  2. Read Conventions. Pitfall: V2 read a timestamp without an offset as UTC and accepted a zone id such as [Europe/Brussels]; V3 returns 400 for both; send the part before [, with its offset. Coordinates become [longitude, latitude]; a swapped pair is not an error, so every job silently comes back unassigned with unreachable.
  3. Read V2 fields with no direct V3 equivalent and Feature parity. Decide each untranslated field’s replacement before you rewrite anything. Pitfall: a leftover V2 key returns 400 with item code unknown_field; the pointer names it.
  4. Read Vehicles, Shifts and Jobs. Wrap the problem in problem, rename to snake_case with unit suffixes, give every shift an id. Pitfall: capacity moves from a positional array of up to 5 to a named map of up to 4 dimensions; a fifth name returns 400 with item code limit_exceeded at /problem/vehicles.
  5. Read Relations and Pickup and delivery pairs. Each PICKUP_AND_DELIVERY pair becomes a shipments[] entry. Pitfall: a relation that names the shipment’s own id returns 400 with item code not_supported; name its legs {shipment}:pickup and {shipment}:delivery.
  6. Read Default objective and From weights to prices. Pitfall: a native V3 request without objective is silently optimised for the V3 defaults, not for what your V2 weights expressed; set each price in objective.costs and on the fields (drop_cost, per_late_hour).
  7. Read Options. ?millis=5000 becomes options.time_limit_s: 5. Pitfall: V2 priced TomTom traffic at request time, while a V3 request without travel.departure_time silently defaults to free-flow osm; set travel.engine: "tomtom" to keep the V2 behaviour. A budget above 60 seconds, which V2 lowered, returns 400 with item code limit_exceeded.
  8. Read Existing plans. Replace initialResource and initialArrival with a request-level plan whose routes name vehicle and shift. Pitfall: a plan freezes nothing, so dispatched stops silently move on re-solve; set plan.routes[].locked_count.
  9. Send the ported body to POST /v3/routing/validate/solve and fix each errors[] item at its pointer, then solve the same instance on both APIs and read it with Response mapping. Pitfall: V2 response times are UTC, V3 writes them in the offset of the shift’s from; a parser that assumes Z silently shifts every arrival. Compare unassigned[], route totals and stop order before switching traffic.
Skip: Quickstart beyond What’s different from V2, the V2 tab, and AI agents.

Build a dispatch UI with warm starts and re-planning

Use this path when dispatchers edit routes during the day. The plan object is the state your UI holds: the API keeps none.
  1. Read Plan and Warm-starting with plan. Store plan from every response. Pitfall: on solve, plan is a hint; an unlocked stop that breaks a hard rule is silently moved or dropped to unassigned[]; set locked_count on the dispatched prefix, or "pinned": true on a stop that must keep its vehicle.
  2. Re-plan by sending problem plus the edited plan to POST /v3/routing/solve. Pitfall: a locked or pinned stop that breaks a hard rule returns 400 with item code invalid_value at /plan; send the same body to POST /v3/routing/evaluate to see which rule.
  3. Read The plan shape and Violations. After each drag-and-drop, call POST /v3/routing/evaluate and render routes[].stops[].violations, routes[].violations and summary.score.violation_count. Pitfall: a window priced with per_late_hour is not a violation when missed, so a UI that renders violations[] alone silently hides late stops; render stops[].lateness_s too.
  4. Read What evaluate checks, and when. Pitfall: an unknown stop id returns 400 with item code unknown_reference, a stop listed twice duplicate_id, a shipment split across routes invalid_value, all at /plan/routes/{i}/stops/{k}, and the real call checks these when it imports the plan; check the plan in your UI first.
  5. Read Reading an insertion and Applying a suggestion. For one new order, add it to problem, leave it out of plan, call POST /v3/routing/suggest, and show placements[].service_start and delta.cost per insertion. Pitfall: each task is ranked against the plan you sent, so applying one insertion silently invalidates the others; call suggest again with the updated plan.
  6. Read When a task gets no insertions. Branch on reason.code, never on reason.message. Pitfall: options.max_insertions above 10 returns 400 with item code limit_exceeded, and 0 returns out_of_range; neither is clamped.
  7. Read Shipments and related tasks if orders are shipments or crews. Pitfall: a shipment with one leg in plan returns 400; leave both legs out.
  8. Read Submit a run, Poll a run and Fetch the plan. For the overnight plan, or more than 400 tasks, POST /v3/routing/runs with the solve body, store the id, poll GET /v3/routing/runs/{id} until status is final, then GET /v3/routing/runs/{id}/result. Pitfall: 401 or more tasks on POST /v3/routing/solve returns 400 with item code limit_exceeded at /problem/jobs; /result before succeeded returns 409 with a message starting run_not_finished.
  9. Read Cancel a run and Limits. Pitfall: a cancelled run counts toward the 10 queued-or-running runs until its worker stops; the eleventh submission returns 429 with Retry-After: 30.
  10. Read Re-optimizing from an existing plan and See how fast the search converges. Size options.time_limit_s for re-plans from search_progress. Pitfall: an omitted seed is 42, so a “try again” button silently returns the same plan; send a different seed. Call the API from your server, never from the browser (Server-side proxy pattern).
Skip: Migration from V2, Constraints beyond the rules dispatchers can break, and AI agents. Field service day walks evaluate and suggest on one dataset.

Model a customer’s operation as an AI agent

Use this path when an agent turns a description of an operation into requests.
  1. Read AI agents: what to ask before you model, and what never to default.
  2. Read the Constraints overview table and map each business rule to one field, hard or soft. Pitfall: a rule the schema has no field for cannot be expressed by inventing one; that returns 400 with item code unknown_field. Tell the user what cannot be modelled instead of approximating it silently.
  3. Send every request to POST /v3/routing/validate/solve before POST /v3/routing/solve. It is not billed. Pitfall: validate does not read plan; the real call checks a warm start.
  4. Read Unassigned jobs and Unassigned tasks. Pitfall: a relaxations[] entry widens a window or reduces a load; applying it silently changes the customer’s rule. Surface it and let the user decide.
  5. Read Problem size. Pitfall: more than 400 tasks on POST /v3/routing/solve returns 400 with item code limit_exceeded; submit a run instead.
Skip: Migration from V2 unless the customer calls /v2/vrp/* today, the V2 tab, and the full OpenAPI document: these pages name every field you need.