Plan lifecycle & statuses
Every status a Routerra Teams plan can hold, the API calls that move it between them, and the transitions the API refuses — the plan state machine.
A plan is built in two steps. Dispatch computes the routes — only your office sees them. Approve sends those routes to drivers. Everything else in the lifecycle follows from that split: editing stops on a plan that already has routes throws it back to a state where the routes are stale, and it needs a dispatch and an approve again before drivers see the change.
If you drive Routerra from your own systems, this is the state machine you are integrating against. Statuses are also what your polling loop keys off after a 202.
How a plan moves
- CREATED goes to DISPATCHING_IN_PROGRESS on dispatch.
- DISPATCHING_IN_PROGRESS goes to DISPATCHED on solver ok.
- DISPATCHED goes to APPROVED on approve.
- APPROVED goes to DISPATCHED on revoke.
- DISPATCHING_IN_PROGRESS goes to DISPATCH_FAILED on solver error.
- DISPATCH_FAILED goes to DISPATCHING_IN_PROGRESS on dispatch again.
- DISPATCHED goes to HAS_CHANGES on edit stops.
- HAS_CHANGES goes to DISPATCHING_IN_PROGRESS on dispatch.
- APPROVED goes to HAS_CHANGES on edit stops.
- CREATED goes to DISPATCH_FAILED on all stops unroutable.
- DISPATCHED goes to CANCELLED on DELETE.
- APPROVED goes to CANCELLED on DELETE.
The teal path is the happy one: create, dispatch, approve. Everything else is a detour off it.
What each status means
CREATED— the plan has stops but no routes. Plans land here when you create them withdispatch=false, when they are copies made through the duplicate endpoint, and when not a single address could be resolved.DISPATCHING_IN_PROGRESS— the solver is working, usually for 5 to 30 seconds. This is the status you poll out of.DISPATCHED— routes exist and belong to the office. Drivers see nothing yet.HAS_CHANGES— someone edited stops on a plan that already had routes, so the routes no longer match the stops. Dispatch again, then approve again.APPROVED— the routes are with the drivers.DISPATCH_FAILED— the solver returned no routes. The reason is indispatchFailureReason, and the optimization token is handed back rather than burned.CANCELLED— the plan was soft-cancelled. Routes are pulled, stops and delivery photos are kept.
| Routes exist | Drivers have them | Listed by GET /plans | |
|---|---|---|---|
| CREATED | No | No | Yes |
| DISPATCHING_IN_PROGRESS | No | No | Yes |
| DISPATCH_FAILED | No | No | Yes |
| DISPATCHED | Yes | No | Yes |
| HAS_CHANGES | Yes | Partial | Yes |
| APPROVED | Yes | Yes | Yes |
| CANCELLED | No | No | No |
HAS_CHANGES is the partial row because it depends on where the plan came from. If it had been approved, the drivers are still holding the routes you sent them; your edits only reach them after the next dispatch and approve. If it had only been dispatched, nothing was ever sent.
Cancelled plans drop out of GET /plans until you ask for them with status=CANCELLED. GET /plans/{planId} keeps returning them, with status: "CANCELLED" — a cancelled plan is not a 404.
The two-step dispatch loop
T+0
Create or dispatch
POST /optimize or POST /plans/{planId}/dispatch answers 202 and the plan turns DISPATCHING_IN_PROGRESS.
5-30 s
Poll for the result
GET /plans/{planId} until the status leaves DISPATCHING_IN_PROGRESS — DISPATCHED on success, DISPATCH_FAILED otherwise.
When you are ready
Approve
POST /plans/{planId}/approve moves DISPATCHED to APPROVED and the routes appear in the driver app.
Mid-shift
Revoke, edit, repeat
Revoke pulls the routes back to DISPATCHED; editing stops sets HAS_CHANGES. Either way, dispatch and approve again.
approve only accepts a plan in DISPATCHED. From anything else you get 409 plan_state_transition — and for a plan in HAS_CHANGES that is the API telling you to dispatch first.
Reading the status over the API
GET /plans/{planId} is the one call that tells you where a plan stands. routes is only present once optimization has produced something — in DISPATCHED, APPROVED, HAS_CHANGES and DISPATCH_FAILED. For a plan in CREATED, the stops are in unassignedStops and unassignedSummary instead.
Response · dispatch accepted
{
"planId": 315,
"status": "DISPATCHING_IN_PROGRESS",
"driverIds": [91, 93],
"revokedBeforeDispatch": true,
"deduplicated": false
}Response · poll, view=concise
{
"id": 315,
"name": "Friday deliveries",
"date": "2026-09-05",
"status": "DISPATCHED",
"hasUnoptimizedChanges": false,
"stopsCount": 3,
"routesCount": 2
// DISPATCH_FAILED adds "dispatchFailureReason"
}hasUnoptimizedChanges is the same fact as the HAS_CHANGES status seen from the field level: it flags a plan whose stops have moved on from its routes.
Cancelling a plan
Soft-cancels the plan. The routes come down and drivers stop seeing it immediately, but the plan, its stops and its proof-of-delivery photos are all kept and the status becomes CANCELLED. It also frees the concurrent-optimization slot if the plan is stuck in DISPATCHING_IN_PROGRESS.
{
"planId": 315,
"name": "Friday deliveries",
"statusBeforeCancellation": "APPROVED",
"cancelled": true,
"status": "CANCELLED",
"restorable": true
}Statuses you will only see on dashboard plans
File-import statuses — IMPORT_IN_PROGRESS and the IMPORT_FAILED* family — only ever appear on plans created from the dashboard, never on plans your integration creates. They matter to an integration in one way: a plan sitting in IMPORT_IN_PROGRESS refuses to be cancelled.
For the same statuses as they appear to dispatchers in the web app, see Statuses reference. For the office-side version of the two-step loop, see Send routes to drivers.