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. A datetime ends with
Z(UTC), carries an offset such as+02:00, or is a local datetime written with a space (2026-10-20 10:00) that the API reads in the plan-leveltzfield (for example,"Europe/Paris"). A local datetime withouttzis rejected, and aZor an offset always wins overtz. Settzin every plan, because it also decides the offset the API uses when it writes datetimes back. See Dates and time zones. - 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.

