Submitting a plan
submit creates the plan and starts its optimization in the same call. It returns the stored plan, with the id the server assigned and the warnings about anything the API amended. Submitting consumes credits with a production key: see Pricing and credits.
submit is never retried automatically, even after a network error: the API has no idempotency key, so a retry could create the plan twice. If a submit fails without a response, check with plans.list whether the plan exists before submitting again.
Waiting for the solution
wait follows the plan until the optimization of its current version is over, then returns the solution:
- It reads the plan’s state every 10 seconds at first, stretching to every 60 seconds on long optimizations, and fetches the solution only once the plan is done. It doesn’t sleep through the whole
maxOptimizationDuration: the engine often stops earlier, once it stops finding improvements. - The solution’s final state says how the optimization ended:
optimized, orinterruptedorstoppedwhen it was cut short. Check it before treating the result as final. - It fails with
KardinalOptimizationErrorwhen there’s no solution to return:NO_SOLUTION(the version ended without one),PLAN_SUPERSEDED(a newer version was submitted meanwhile), orOPTIMIZATION_STALLED(still optimizing 5 minutes past itsmaxOptimizationDuration). A plan deleted while waiting fails withKardinalNotFoundError. - The optional time limit (
timeoutin seconds in Python,timeoutMsin milliseconds in TypeScript) fails withKardinalTimeoutErroronce elapsed, after checking the state one last time.
Tracking progress without blocking
To follow a plan without waiting, read its state and fetch the solution yourself:solutions.get returns the current best solution right away, even while the engine is still improving it. Right after submit, it fails with a 404 until the engine has a first solution. Like any manual tracking, read a plan’s state no more than once every 10 seconds.
Waiting in a web backend or serverless function
wait is meant for scripts, notebooks, and workers. In a request handler, don’t hold the request open while the engine runs:
- Submit the plan, store its
id, and return right away. - Read the state later — from a scheduled job, a queue worker, or the next request from your front end — and fetch the solution once the plan is done.
- A serverless function usually has a shorter time limit than an optimization: track the plan across invocations instead of waiting in one.
- Python
- TypeScript
The client is synchronous. In an
asyncio app, run blocking calls in a thread (await asyncio.to_thread(k.routing.solutions.wait, plan_id)) so they don’t block the event loop, or track the plan with get_status, as shown earlier.Re-optimizing with update
When something changes — a new order, a cancelled one, a vehicle out of service — update the plan rather than submitting a new one. The engine continues from the current solution instead of starting over: see Real-time re-optimization.
updatereplaces the whole plan: anything left out is removed. Start from the plan returned byget,submit, or a previousupdate, and modify it.- The version increments, and the plan is re-optimized, only if the content actually changed. An identical update is a free no-op that keeps the same version, which makes replaying an
updateharmless. - A field the API added after your SDK version, read by
get, is kept and sent back, so updating with an older SDK doesn’t delete it. - An archived plan refuses an update without
force, with a403whosedetailsisCannot update an archived plan, except with the 'force' query param.force=True(Python) or{ force: true }(TypeScript) unarchives it first. On an archived plan, a forced update always creates a new version and re-optimizes the plan, even if its content is identical, so every stop is billed again, as for any update or restart made more than 24 hours after the plan’s creation. On any other plan,forcehas no effect. Don’t set it by default.
Starting and stopping optimization
agencyId, planId, planVersion). A restart with start is billed like an update, by the 24-hour rule of Pricing and credits, and it counts as one optimization against your agency’s daily and monthly optimization quota (see Limits and quotas). An archived plan refuses start and stop without force, with the same 403 as for update. force unarchives the plan first, for a stop too. An archived plan is more than 24 hours old, so a forced restart bills every stop again.
Listing plans
get for the full plan. Archived plans are left out by default; pass archived="included" or "only" (Python) or { archived: "included" } (TypeScript) to change that. A page past the last one returns the last page: its page field gives the page actually returned. See Paginating results.

