Skip to main content
This walkthrough builds a more realistic plan than First API call: two vehicles starting from two different depots, delivering to stops with time windows. It assumes you’ve already authenticated — see Authentication if you haven’t.

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 an id, a vehicleProfile, and a workingTimeWindow; each order needs an id and at least one stop with a position.
Submit this the same way as in First API call — 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 costs delay, tracked by the minimizeDelay objective, but the stop still gets served.
  • Only use authorizedTimeWindows if 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 to preferredTimeWindows for contractual windows unless you’ve explicitly confirmed otherwise with the business.
If you leave 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.
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 computed arrivalTime.
  • 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 as preferredTimeWindows earlier, this should be empty; if you’d used authorizedTimeWindows instead and a window were unreachable, the stop would show up here instead of arriving late.
  • tours[].distanceInKm and tours[].workingDuration, useful for a sanity check against what you’d expect for the geography.
If a stop you expected to be served ends up in 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, not 30); 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 tz field 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’s departure/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 authorizedTimeWindows just because it’s contractual — that choice silently drops stops rather than delivering them late.

See also