The scenario
Two vans, each starting and ending its day at a different depot, deliver to four stops with client-facing delivery windows.Step 1 — start minimal: resources and orders only
Get a feasible plan running before adding any constraints. Each resource needs at minimum anid, a vehicleProfile, and a workingTimeWindow; each order needs an id and at least one stop with a position.
POST /plans (the service generates the plan’s id) — and confirm that all four stops are planned. With the default objectives, which include minimizeResources, expect a single tour: one van can serve all four stops within its working day, so the engine leaves the other one unused. Positions here are illustrative — see Positions and geocoding for why real positions must be pre-geocoded before you submit them.
Step 2 — add depots as departure/arrival
The preceding plan already has each van start and end at a depot position rather than at the first/last stop — this is what makes the two-depot scenario realistic. If departure/arrival are omitted, the working day starts and ends at whichever stop the engine happens to assign first/last, which is rarely what a dispatcher expects for a fleet with fixed depots.
Step 3 — add delivery time windows, and choose hard vs soft deliberately
Now add a delivery window to each stop. This is the step where it’s easy to get the modeling choice wrong: a client-facing or “contractual” window is not automatically a hard constraint. Ask what should happen if the fleet can’t hit the window exactly:- If a late visit is still worth making — the customer would rather get a delayed delivery than none — use
preferredTimeWindows. Missing it costsdelay, tracked by theminimizeDelayobjective, but the stop still gets served. - Only use
authorizedTimeWindowsif a visit outside the window genuinely can’t happen (site closed, access refused). See Hard vs soft constraints for the full reasoning — the short version is: default topreferredTimeWindowsfor contractual windows unless you’ve explicitly confirmed otherwise with the business.
objectives out, the default list already includes minimizeDelay — see The default list. If you set your own list, keep maximizeMandatoryStops first: a list with only minimizeDelay gives the engine no reason to plan any stop at all.
Full plan for this step
Full plan for this step
The plan from step 1 with a preferred window on each stop and the two-objective list. With only these two objectives, nothing asks the engine to save a van, so it may spread the stops over both.
Step 4 — read the result
Fetch the solution the same way as in First API call and check, per tour:tours[].wayPoints— the assigned stops, in visiting sequence, each with a computedarrivalTime.tours[].isValid— whether that specific tour respects all hard constraints; check this per-tour, not just at the plan level.unaffectedStopIds— any stop that couldn’t be placed at all. With everything modeled aspreferredTimeWindowsearlier, this should be empty; if you’d usedauthorizedTimeWindowsinstead and a window were unreachable, the stop would show up here instead of arriving late.tours[].distanceInKmandtours[].workingDuration, useful for a sanity check against what you’d expect for the geography.
unaffectedStopIds, see Handling infeasibility for how to diagnose which constraint caused it.
Common pitfalls at this stage
- Units. Distances in the API are kilometers, not miles; durations are ISO 8601 (
PT30M, not30); make sure any values sourced from a spreadsheet are converted before submission. - Time zones. Datetimes without an explicit UTC offset are ambiguous — either include the offset on every datetime, or set the plan-level
tzfield once (for example,"Europe/Paris") and write local, offset-free datetimes. Mixing both styles in the same plan is a common source of off-by-a-few-hours bugs. - Positions. Every
position(stops, and a resource’sdeparture/arrival) must be a{lat, lon}pair in decimal degrees: the API does not geocode addresses. Geocode them on your side before building the plan, or contact customer.success@kardinal.ai to have an address set geocoded on request. Spot-check the result, because a wrongly geocoded position raises no error: it silently routes to the wrong place. See Positions and geocoding. - Hard vs soft defaults. As shown in Step 3, don’t default a contractual time window to
authorizedTimeWindowsjust because it’s contractual — that choice silently drops stops rather than delivering them late.
See also
- Data model — an overview of everything used earlier, each part linking to its schema in the API reference.
- Hard vs soft constraints — the general reasoning behind the Step 3 choice.
- Handling infeasibility — what to do when a stop doesn’t get planned.
- Modeling advanced constraints — the next step once a two-vehicle, time-windowed plan is working: capacities, skills, breaks, and multiple time windows.

