Skip to main content
A plan is built from two lists — resources (vehicles/drivers) and orders (what needs to be done) — plus plan-level fields that add constraints spanning several of them. This page is the field dictionary; see First API call for the request/response shapes and How the optimization engine works for what the engine does with these fields.

Resource (vehicle-driver pair)

A Resource represents one vehicle-driver pair for the duration of the plan. Only id, vehicleProfile, and workingTimeWindow are required — everything else defaults to “unconstrained.”
capacities, skills, and tags are all free-form: the API doesn’t predefine weight or forklift as special values. Whatever keys you use on a resource must match the keys used on the stops/orders you expect it to serve.

Modeling a multi-day plan

workingTimeWindow isn’t limited to a single calendar day. Sizing it to span several days — or a full week — is the correct pattern whenever an order’s stops legitimately fall on different dates and all of them still need to be planned onto one resource: a pickup stop on one day and its paired delivery stop two days later, for example, requires a resource whose workingTimeWindow brackets both timestamps, because an Order’s stops must all be planned onto the same resource (see Order, Stop). Splitting the same fleet into a separate Resource per calendar day instead breaks that pairing: no single one of those daily resources could serve both the pickup and the delivery stop of the same order.
A wide workingTimeWindow does not, on its own, force any rest between working days — nothing about the field stops the engine from scheduling a stop late on one day and another early the next. If a daily-rest requirement applies, it must be encoded independently in breaks[] (see Breaks): a multi-day window and a daily-rest rule are two separate mechanisms, and setting one is not a substitute for the other.
One concrete way to encode daily rest inside a multi-day workingTimeWindow: repurpose travelDurationSlidingBreak with duration figures sized for a full night’s rest rather than an in-shift driving break — for example minBreakDuration around PT11H (the rest period itself) triggered after maxInterBreakDuration around PT9H of cumulative driving (the daily driving allowance before rest is due), so the sliding-break mechanism produces one long rest per day instead of several short ones. A second, equally valid mechanism is stacking one timeWindowBreak entry per calendar day the plan spans, each pinned to a fixed nightly clock time (for example 22:0006:00 on every date in the window) — better suited when the rest period should anchor to the clock rather than to cumulative driving time. Either way, the daily-rest requirement needs its own explicit breaks[] entry; it is never implied by workingTimeWindow alone. If the daily-rest pattern above and an in-shift driving break (see Breaks) both apply to the same resource, the recommended approach is two separate travelDurationSlidingBreak entries in the same breaks[] array rather than trying to compress both purposes into one — a short-threshold entry for the in-shift driving break (e.g. PT45M after PT4H30M) alongside a long-threshold entry for the daily-rest cycle (e.g. PT11H after PT9H). Each entry is its own object with its own minBreakDuration/maxInterBreakDuration pair, and the data model places no restriction on repeating the same breaks[] type with different thresholds — this is the documented way to express both requirements at once, rather than a sign that one purpose needs a different break type from the other.

Vehicle profiles

vehicleProfile.type determines both the routing mode and which extra parameters are available:
Setting type: "truck" and leaving every truck-only field unset does not silently apply any default legal-truck-road restriction — routing behaves exactly like car until you actually set one of those fields. So the choice between car and truck only matters once you populate at least one restriction field; if you don’t know a vehicle’s dimensions yet, default to car (a delivery van with no known dimensions is a car, not an under-specified truck) and switch to truck once real dimensions or road restrictions are available.This default is for the case where dimensions are genuinely unknown, not for the case where the vehicle’s own name already answers the question. If a vehicle-type name in your source data is itself an unambiguous heavy-vehicle designation (a truck, lorry, semi-trailer, tractor-trailer, or an equivalent term in the client’s own language) rather than a generic label like “van” or “car”, treat that name as sufficient evidence to pick truck even with no numeric dimensions yet — don’t fall back to car just because the restriction fields are still empty. The no-dimensions default exists for genuinely ambiguous or generic vehicle descriptions, not for names that already name a heavy-vehicle class.
withTraffic: true (available on road-network profiles) enables predictive traffic, billed separately — see How the optimization engine works.
For any plan where preferredTimeWindows / authorizedTimeWindows matter — which is almost every real delivery scenario — default to withTraffic: true. Travel-time estimates computed without it are systematically optimistic, which silently erodes on-time performance against exactly the time windows you’re trying to hit. Confirm the added billing with the client/account team, but don’t let cost-consciousness alone default this to off: it isn’t something to leave out by default just because nothing in your source data mentions it.

Order, Stop

An Order is one or more stops that must all be planned onto the same resource, in array order (position in the array acts as a precedence constraint). Only id and stops are required. A stop is either a SingleStop (only id and position are required) or an AlternativesStop — a list of SingleStop candidates the engine picks the best one from (for example several possible disposal sites; increases optimization time). See Optional steps that gate a mandatory stop for a worked example of using this to model a preceding step that’s only needed in some contexts, rather than a fixed choice among equivalent sites. The real-world direction a stop moves goods in is carried by the sign of capacities, not by kind. kind only names the generic operation (a collection, a drop-off, or a no-cargo intervention) and fixes the add/deduct/ignore rule above; it is not itself proof of which physical direction goods move. A dataset can use kind: "pickup" on every single stop in the plan and still model both collections and drop-offs correctly, simply by flipping the sign of capacities instead of switching kind. Treat this as the load-bearing rule for kind — not a side note: get it wrong and a payload can look plausible while silently tracking every stop’s load in the wrong direction.
If you need to reconstruct the real-world direction of a stop after the fact (for a report, for instance), read the sign of its capacities values together with kind — the combination, not kind in isolation, tells you whether the resource’s load actually went up or down at that stop.
Id-uniqueness is scoped per collection, not global: resources[].id, orders[].id, and stops[].id are each their own namespace, so the same string can be reused across resources, orders, and stops in the same plan without a collision.
Always set the type field on a stop explicitly ("type": "single" for a SingleStop), even though the API defaults it and a bare stop object will be accepted as-is. A discriminated-union validator (for example a typed SDK re-parsing your own payload to confirm it round-trips) needs the tag present in the data itself to pick the right stop variant, and rejects an object that’s missing it even when that field has a documented default — so a payload that only works because the tag was left to its default won’t survive that kind of check.
operationDuration has no built-in formula — it’s a plain duration you compute upstream from your own business data (for example a fixed per-stop time plus a variable component based on quantity delivered). Nothing on the API side derives it for you; get this calculation wrong and the plan stays valid (no error), just unrealistic.
Kardinal does not geocode addresses — every position you submit must already be a {lat, lon} pair. If your source data (a client spreadsheet, a CRM export) only has addresses, convert them to coordinates with a geocoding provider (for example Google Maps Geocoding API, Mapbox Geocoding, or the BAN/Base Adresse Nationale API for French addresses) before building the plan. Getting this wrong doesn’t raise an error — a valid-looking but incorrectly geocoded position (e.g. a commune centroid instead of the real address) still produces a plan, just one that’s routed to the wrong place.

Capacities

capacities is a free-form map ({"weight": 2200, "volume": 9.5, "nbPackages": 23}) on both resources and stops — any key you invent is tracked as its own independent dimension. A resource can only serve a stop if every capacity key the stop consumes is also declared on the resource, and the running total never exceeds the resource’s value for that key at any point in the tour. See Modeling advanced constraints for multi-compartment vehicles and tolerated overflow.

Count-based vs. size-based ceilings for “one item at a time” vehicles

Some vehicles carry exactly one item at a time, but the item’s own size varies from trip to trip — for example a truck that carries a single container, skip, or tank per load, where different units hold different volumes. Two capacity representations both correctly enforce “never more than one aboard,” with a real trade-off between them:
  • Count-based ceiling — for example capacities: {"containers": 1} on the resource and on every stop that adds or removes a unit. The ceiling tracks how many items are aboard, not their size, so it’s robust to item-size variance: a fleet with mixed unit sizes needs no per-trip capacity tuning, and you never need to know or record any individual item’s real volume for the constraint to work.
  • Size-based ceiling — for example capacities: {"volume": <max observed item size>}, sized to the largest unit the resource can ever carry. This loses the size-invariance of the count-based approach (a resource sized for the largest unit looks under-loaded whenever it’s actually carrying a smaller one), but it’s the representation that supports overflow or partial-load reporting — a stop that only partially fills the ceiling, or exceeds it, is visible in the numbers. A count of 1 can’t express that: it’s already at its ceiling regardless of the item’s real fill level.
Pick count-based when the only requirement is “one item at a time” and nothing downstream needs the item’s size; pick size-based when the capacity value itself needs to carry fill-level or overflow information. A capacity ceiling isn’t the only way to keep a “one item at a time” vehicle from being double-booked, and isn’t always sufficient on its own: when swapping the item at a site has its own real handling-time cost (dropping off the current unit before picking up the next one), tagging the relevant stops and charging that duration with the plan-level setupDurations field — combined with successiveStops on the order — is a separate, valid mechanism, one that a bare capacity ceiling can’t express on its own.

Shared capacity pools

By default, a resource’s capacities are just its own running load — nothing ties one order’s stops to another’s beyond sharing the same ceiling. Set the plan-level sharedCapacities to true (see Plan-level fields) when several different orders assigned to the same resource need to draw down and top back up the same running balance — for example a limited stock of reusable equipment or exchangeable containers that a resource carries and moves between stops over the course of one tour, rather than a fixed cargo that’s loaded once and only ever decreases.
order-1’s stop deducts 10 units from equipmentPool, and order-2’s stop — from a different order, later in the same tour — adds 10 back. The two only interact because sharedCapacities is true: without it, nothing guarantees that stops from separate orders read and write the same running balance on the resource. Pair sharedCapacities with an atLeastOneValidCapacity additional constraint whenever the business rule is stronger than “never exceed the ceiling at any single point” — for instance requiring the pool to reach a specific state at some point in the tour. The example above uses the pool’s own ceiling as the threshold; tighten it (for example to a value close to zero) if the rule requires the pool to actually return to a stricter level at some point, rather than just staying under its maximum. A related pattern is when the step that tops the pool back up is itself optional — needed only when the pool hasn’t already been replenished earlier in the tour. That case is usually scoped to a single order (the optional fetch and the mandatory stop it feeds are two stops of the same order, not two separate orders), so it doesn’t need sharedCapacities at all — the default per-order pooling above is enough. See Optional steps that gate a mandatory stop for the worked example, including why it decouples the pool into two separate capacity keys instead of one net key. Reach for sharedCapacities on top of that pattern only if the fetch and the stop it feeds end up split across genuinely different orders on the same resource.

Time windows

Two related shapes:
  • TimeWindow — plain {begin, end}, used for workingTimeWindow and break windows.
  • TaggedTimeWindow — the same shape plus an optional resourceTags array, used for authorizedTimeWindows / preferredTimeWindows on a stop. When resourceTags is set, that window only applies to resources carrying a matching tag (for example an early slot reserved for a certified subcontractor); resources without the tag only see the untagged windows.
A stop’s authorizedTimeWindows accepts an array, so more than one disjoint hard window is native (a site open mornings and again in the evening, for instance) — the stop is feasible if reachable in any one of them. See Hard vs soft constraints for how preferredTimeWindows interacts with the authorized ones.

Breaks

A resource’s breaks array mixes any of three types — they aren’t mutually exclusive, and breaks count as working time unless the type says otherwise. The two sliding-break types count the same physical break toward both clocks — define both together for a shift with both a labor-time rule and a driving-time rule, rather than assuming one covers the other. For a resource whose workingTimeWindow spans more than one calendar day, breaks[] is also where daily rest has to be encoded — a wide window does not produce it on its own. See Modeling a multi-day plan for a worked pattern.
The API has no way to infer maxInterBreakDuration (or which sliding-break type applies) from a break duration alone. Source data often gives you only “30 min break” with no stated trigger — that’s a labor-agreement or ops-policy detail, not something derivable from the break length itself. Don’t default to a generic legal minimum (e.g. a national labor-law figure) without confirming it against the client’s actual collective agreement or dispatch rules — the two are frequently different, and the gap directly changes where breaks get scheduled.If you need to submit a first end-to-end payload before the client’s break rules are confirmed, a timeWindowBreak spanning most of the shift (rather than a guessed sliding-break trigger) is a safer placeholder — clearly flag it as provisional and pending client confirmation rather than treating it as correct.

Constraints catalog

Beyond what resources/orders/stops express directly, a plan can carry additionalConstraints (scoped to specific tags) and globalConstraints (scoped to the whole fleet):
removalStrategy: "lifo" is the mechanism for a stacked or sequential-loading vehicle — a car carrier loading vehicles nose-to-tail on a single deck, or a multi-deck cage truck — where physically nothing can be unloaded except the item that went on last. Set it on the resource tag(s) that represent that vehicle class; the engine then only sequences stops in an order consistent with last-in-first-out access, instead of allowing a mid-stack item to be unloaded before the ones loaded after it.
overlappingCapacitiesByStopTag (plan-level, paired with the minimizeOverOverlappingCapacitiesOnStops objective) is a related but distinct mechanism: it caps how many resources can be physically present at the same time at stops sharing a tag — the native way to model a shared bottleneck resource such as a depot with a limited number of loading docks, or a cross-dock with limited simultaneous capacity. accessDurationsByStopTag (plan-level) is a third tag-based mechanism, easy to confuse with the two above: it adds a fixed extra duration once, before the first of a group of consecutive stops sharing a tag — for example a single 30-minute dock access time charged once per depot visit, no matter how many individual pickups happen during that visit. This is distinct from a stop’s own operationDuration, which is charged on every stop individually regardless of what came before it: use accessDurationsByStopTag for a cost paid once per visit to a tagged location, and operationDuration for a cost paid per stop. All of the above are enforced as hard constraints unless the mechanism is explicitly cost-based (maxCumulatedCost, custom cost objects) — see Hard vs soft constraints for the general distinction.

Plan-level fields

A few fields configure the plan as a whole rather than any single resource or order:

Depot vs. position

Depot is a standalone, agency-level object (id, name, position, address, isMain) conceptually distinct from a plan’s Resource.departure/arrival positions. Its own CRUD endpoints are not part of this API reference — don’t expect to find them by browsing these pages or the openapi.yaml spec here, and don’t spend time searching for them. It is not referenced by id inside a plan either way: a resource’s departure/arrival normally take a raw {lat, lon} position — typically resolved from one of your Depot objects. arrival (but not departure) can alternatively be set to the literal string "atFirstPosition" instead of a position, meaning the resource returns to wherever its tour actually started rather than to a separate, fixed depot point — useful when a tour should end where it began without pinning that point to a specific coordinate ahead of time. If you manage depots as first-class objects on your side, resolve them to coordinates yourself before building the plan — there is currently no depotId field linking a resource back to a Depot. The Depot object also has no opening-hours field; model a depot’s operating hours through the workingTimeWindow of the resources that start/end there. These are two distinct mechanisms with two very different costs of adoption — don’t reach for the second one when the first is all you need.

Opening hours vs. shift window: the depot’s opening hours win

If your source data gives you both a site/depot opening-hours figure (e.g. “warehouse open 05:30–16:00”) and a separate per-vehicle shift window (e.g. a driver’s “06:00–23:59” shift on a timesheet), don’t default to the vehicle-level figure alone, and don’t intersect the two either — set the resource’s workingTimeWindow to the depot’s opening hours. In practice, a per-vehicle shift window this wide relative to the depot hours is typically a payroll/administrative boundary (the driver’s paid shift), not a physical-availability constraint — it doesn’t narrow what the vehicle can actually do at the depot, so it shouldn’t narrow workingTimeWindow either. Only fall back to intersecting the two (or to the vehicle figure alone) if you’ve confirmed with the client that the per-vehicle window reflects a real operational constraint (e.g. a driver who genuinely cannot start before a fixed time regardless of the depot being open) rather than an administrative one.This is a plain workingTimeWindow computation — it doesn’t require modeling the depot as a stop, and it’s unrelated to the pattern below.

Loading docks, dock time, or reload trips: model the depot as a tagged stop

A depot with limited simultaneous dock capacity, a fixed per-visit dock/access time, or a fleet that needs to reload there mid-shift is a different, heavier pattern: it requires restructuring orders to include an explicit pickup stop at the depot (tagged, e.g. depot:main), plus plan-level accessDurationsByStopTag and/or overlappingCapacitiesByStopTag. Unlike the intersection rule above, this is a structural change to your orders array — see Multi-trip tours for the full worked pattern, including when it’s actually needed (compare total stop demand against total fleet capacity first).

See also