Skip to main content
V3 is in beta. General availability (GA) is planned for January 2027.
Every request asks the solver two questions: which plans are allowed, and which allowed plan is best. Your constraints answer the first and your objective answers the second. This page explains the model behind both for POST /v3/routing/solve. For each field and the JSON to send, see Constraints. On this page a task is a job or a shipment. Where a rule applies to jobs only, the page says “job”.
Coming from V2? There are no constraint weights and no hard/medium/soft score levels here. A rule is either hard (it always holds) or it is a price in your own currency. See Migration from V2.

Hard and soft

Every rule you send is one of two kinds: A solve result never contains a broken hard rule: summary.score.violations is always 0 on /v3/routing/solve. To check a plan of your own, which may break hard rules, use /v3/routing/evaluate.

Hard constraints

Only the rules you send are active. A request with capacities and nothing else is checked for capacity and nothing else.

Capacity

Dimensions are named and matched by name between capacity and pickup.

Time windows

A window is hard unless you price lateness. This job must be reached between 09:00 and 12:00:
Several windows on one task mean “any of these”. Arriving early always waits for from; only the late side can be made soft.

Relations

relations[] ties jobs to each other. It has nine types, and all of them are hard.
An unknown relation type is rejected with HTTP 400.
Relations act on jobs, not on shipments. A shipment id in the stops of a sequence, same_vehicle, same_day or synchronized relation is accepted but not constrained: the request is not rejected, and the relation has no effect on that shipment. The same holds for a shipment that carries the tag of a same_vehicle or same_day relation. Five cases are rejected with HTTP 400: a shipment id in a time_lag, a shipment id in a compatible_vehicles relation, a shipment id in an all_or_none relation, a tag in a tag_order relation that selects a shipment, and a shipment named in or selected by a same_route relation.
A shipment already has one built-in rule of its own: the pickup comes before the delivery, on the same route. See Relations for every field and every rejected combination.

Soft constraints

Use these levers to allow something at a price. Each one reports what it cost in summary.score.components. How they combine:
  • Lateness, per window or fleet-wide. Use time_windows[].per_late_hour when only some tasks may run late: it softens that one window and leaves every other window hard. It must be greater than 0 and is allowed only on a task with exactly one window. Use objective.per_late_hour to soften all windows at once; a window with its own rate keeps that rate. Omit the fleet rate to keep windows hard: an explicit 0.0 is rejected with HTTP 400.
  • Alternative-window cost does not soften anything. Every window stays hard; cost only grades which of a task’s two or more windows is preferred. A non-zero window cost anywhere in the request cannot be combined with objective.per_late_hour (HTTP 400).
  • A target is a point, not a window. It never makes a task unassignable and never forces a vehicle to wait. On a shipment it is measured on the delivery leg. The stop reports target_lateness_s.
  • Overtime is bounded. max_overtime_s grants the extension; anything beyond to + max_overtime_s is still forbidden. Without per_overtime_hour overtime carries no extra charge beyond the normal hourly route rate.
  • Preference entries are independent. A task listing several preferred vehicles pays for every listed vehicle that does not serve it, including when one of the others does. A vehicle with several preferred_tags charges each tag the task lacks.
A job with a soft window, a target, and a preferred vehicle:
Three more priced fields shape a plan without softening a rule: drop_fee makes a task optional at a price, preferred_date with objective.non_preferred_date_cost steers a task to a day, and objective.grouping keeps nearby tasks together. Skills, capacity, eligibility, relations, limits and quotas have no soft form.

From constraints to the objective

Among the plans that respect every hard rule, the solver compares in a fixed order: least unserved_priority first, then least cost. Serving required tasks always comes before saving money; every soft lever above is part of cost. Objective function covers the comparison, priority, and the cost model in full. You set cost in two places: fleet-wide rates under objective.costs, and per-vehicle terms under vehicles[].costs.
Put a shift’s start and end on the shift itself, as start and end, each naming a location or a depot.

Why a task is left unassigned

When the solver leaves a task out of the plan, the solve response says why. Each task left out has an entry in unassigned[]; you do not need an extra request or flag.
Read the entry in this order:
  1. category tells you what kind of fix can work. impossible and blocked mean a hard rule is in the way: change the request, or add capacity or working time. priced_out means the task has a drop_fee and paying it was cheaper than serving the task. search_limit means a worthwhile place exists and the time budget ran out first.
  2. reasons[].code names the rule that blocked the task. One code is reported per task.
  3. relaxations[] gives the smallest change that would have let the task fit, when one number can express it: reduce_demand (in load units) for capacity, extend_time_window (in seconds) for time windows. It is empty for rules such as skills, where the fix is structural. field names the task by its id, under /jobs/ or /shipments/; it is a label, not a path into your request body.
Errors lists every category and reason code with what to check for each. Tasks that were routed carry no explanation. See Explainable AI for worked examples.

Performance impact

The solver checks only the rules your request uses, so a plain capacity problem is the fastest kind to search. Each additional kind of rule (time windows, skills, relations, shipments, breaks) adds work to every candidate move, which means fewer moves explored in the same options.time_limit_s. Send the rules your operation needs and leave out the ones it does not. See the Performance guide for the other levers.

Best practices

  1. Use a hard rule for anything that must always hold. Capacity, skills, eligibility and limits belong in their own fields, not in a large cost.
  2. Pick the narrowest lateness lever. Soften one task’s single window with time_windows[].per_late_hour, soften all windows with objective.per_late_hour, and use target for a deadline that must never make a task unassignable.
  3. Make a task optional with drop_fee. Tasks are required by default.
  4. Rank required tasks with priority. When not every required task fits, the solver keeps the higher-priority ones. See Objective function.
  5. Read unassigned[] before tuning costs. Cost rates do not rescue a task that a hard rule blocks; category, reasons and relaxations tell you what does.
  6. Do not rely on a relation to constrain a shipment. Relations act on jobs. To tie a shipment to a vehicle, set locked_vehicle or eligible_vehicles on the shipment.
Fields are validated strictly: an unknown or misspelled field, and contradictory combinations such as drop_fee and priority on one task, are rejected with HTTP 400 and a message describing the problem. See Errors.

Constraints

Every constraint field, with the JSON to send

Objective function

How plans are compared, priority, and the cost model

Explainable AI

Worked examples of the unassigned[] diagnosis

Performance guide

How problem size and constraint choice affect solve time