Skip to main content
Kardinal’s Route Optimization API is built around two steps: you submit a plan describing resources, orders, and constraints, and the engine continuously searches for a solution. Understanding how that search behaves — not just the request/response shapes — is what lets you tune it for your business instead of treating it as a black box.

What the engine optimizes

Optimization is driven by an ordered list of objectives. The engine compares candidate solutions one objective at a time, in that order: the first objective that separates two solutions decides which one wins, whatever happens on the later ones. Order reflects business priority, not a weighted average. The default sequence covers most cases (99% according to Kardinal):
See Objectives and how they’re ranked for how two solutions are compared, what each objective measures and what data it needs, and how to choose an order for your plan.

The quality vs. computation time trade-off

There is no single “optimize until done” call, and no synchronous alternative that blocks until a solution is ready — the API is asynchronous only: submitting a plan returns as soon as it’s accepted, and you retrieve or poll the solution separately. Instead, you control how much time the engine is allowed to spend via maxOptimizationDuration (an ISO 8601 duration, for example "PT10M"):
  • The engine is guaranteed to never regress: each new solution it publishes is at least as good as the previous one on the preceding objective sequence. There’s no risk of “rolling back” to something worse.
  • The longer you let it run, the better the solution can get — but returns diminish. If the engine hasn’t found an improvement in a while, it considers itself done, even before maxOptimizationDuration elapses.
  • maxOptimizationDuration only counts time actually spent searching. It excludes queueing time (if no worker is free), and the time spent building the underlying math problem (fetching travel times, etc.). Add a margin before you fetch a solution to account for this.
This gives you three practical integration patterns:
Set a long maxOptimizationDuration (for example, PT30M), submit the plan, and fetch the solution after that window (plus margin). Simple, but you wait for the full window even if the engine converged early.
Use a short maxOptimizationDuration and, if the solution isn’t good enough, restart optimization with PUT /plans/{planId}/running (body true). Updating the plan with changed data has the same restarting effect, but resubmitting an identical plan doesn’t: if nothing in the plan’s JSON has changed, optimization isn’t relaunched, its version stays the same, and it isn’t billed, so use the running endpoint to restart an unchanged plan. Restarting won’t help if the duration is too short for the problem size, or if the engine already considers the current solution final.
Poll GET /plans/{planId}/state — no more than once every 10 seconds per plan, less often on long optimizations — and read the solution and its objective values as it improves, deciding for yourself when the result is “good enough” for your business, without waiting for the engine to fully settle. This is the most responsive pattern and the one Kardinal recommends; see Polling the state and the solution for what to expect along the way.
The status field tracks a plan through its lifecycle — waiting room (if you’re over your max simultaneous running plans quota), creation (fetching travel times, building the problem), optimization, and, if predictive traffic is enabled, an asynchronous traffic-fetching stage running in parallel:
state summarizes that same lifecycle as a single value (waiting, processing, preOptimizing, preOptimized, optimizing, optimized, stopped, deleted, interrupted) instead of a breakdown per stage — optimized is the value to poll for once you only care about “is this done,” rather than inspecting every field inside status. Retrieve it directly with GET /plans/{planId}/state:
Response (abridged)
state tells you the plan has settled — it doesn’t carry the solution itself. Once state is optimized, fetch the actual result with GET /plans/{planId}/solution.

Polling the state and the solution

  • Poll /state no more than once every 10 seconds per plan, and space calls out further on long optimizations.
  • optimized isn’t the only end state. A plan stopped on request goes to stopped, then to interrupted a few seconds later once the engine has actually stopped — the solution can still improve between the two. A version is also interrupted when a newer version of the plan is submitted. Always set a maximum wait on the client side, in case the state stops progressing.
  • Before the first solution exists (waiting, processing), GET /plans/{planId}/solution returns 404 NOT_FOUND with properties.details: "Solution version not found." — not an error to give up on, just a solution that isn’t there yet.
  • The first solution published is often empty: every stop in unaffectedStopIds and no tours, published as soon as optimization starts, with the real solution following shortly after. A 200 on /solution doesn’t by itself mean the solution is usable.
  • After a PUT, /solution keeps returning the previous version’s solution until the new version has one of its own — and indefinitely if the new version is stopped before being optimized. Compare the solution’s planVersion with the version you expect; /state returns planVersion too, telling you which version the state refers to.

Continuous and interactive optimization

Submitting a plan again with the same id doesn’t start a new problem from scratch — it tells the engine “this is the same problem, with updated data.” The request still carries the complete plan, not only what changed (see Real-time re-optimization). If the plan’s content changed, the version number increments and the engine repairs the previous solution, adjusting it just enough to be valid for the new data, then keeps improving from there; an identical plan changes nothing. This first optimization on a given plan layout is slower (the engine is learning the problem’s shape); subsequent updates are typically much faster. Because of this, if you update a plan while an older version is still optimizing, the engine does not let two versions be considered “optimized” at the same time — it stops work on the stale version in favor of the newest one.

What you control vs. what the engine controls

You control: the objectives list and its order, maxOptimizationDuration, when to restart or stop optimization (running: false), resource/order priority, and lateDeparture (whether resources leave as early as possible or depart later to reduce idle waiting time). The engine controls: the actual search strategy, and the guarantee that solution quality never regresses between versions. Problem complexity — and therefore how much maxOptimizationDuration you should budget — is mostly driven by the number of stops and resources, and whether advanced constraints (alternative stops, LIFO removal strategy, overlapping-capacity limits) are in play; some of these switch the engine to alternative algorithms that are markedly slower. Traffic-aware vehicle profiles (withTraffic: true) don’t change the search itself: they only lengthen the fetching of travel-time matrices, which isn’t counted in maxOptimizationDuration. Allow for that extra time in how long you wait before fetching a solution, not in maxOptimizationDuration. See Hard vs soft constraints for how the engine decides what’s non-negotiable versus what it can compromise on while searching for these objectives.

See also