Response objects & enums
Field reference for the Routerra Teams API — stop, route and change objects, every enum value they can hold, what the API deliberately doesn't do, and what changed.
The field reference for everything the external API hands back. Two conventions apply everywhere: fields that are null are omitted rather than sent as null, and units differ per field — metres, minutes and seconds all appear, so check Dates, times and units before you multiply anything.
Stop
The ExternalStopDto. A stop carries what you sent, what optimization worked out, and what the driver later did with it — grouped here by which of those three it is.
| Group | Fields |
|---|---|
| Identity | id name address latitude longitude addressBookEntryId metadata |
| Settings | serviceTime (seconds) timeWindows[] priority stopSide load assignedDriverId note |
| Recipient | email phone (E.164) recipientName |
| Optimization result | position expectedArrival expectedArrivalLocal driveTime (minutes) driveDistance (metres) waitTime (minutes) errorType |
| Delivery | status statusChangedAt statusChangedAtLocal failureReason |
metadata is an opaque blob: whatever JSON you send on a stop comes back unchanged, nested objects and arrays included. Routerra never reads it — which also means you cannot search on it.
Route
The ExternalRouteDto. One route is one driver's day.
| Group | Fields |
|---|---|
| Route | id driverId driverName status timezone distance (metres) time (minutes) stopsCount startLocation finishLocation |
| Progress | deliveredCount failedCount pendingCount startedAt completedAt nextStop with id, name, position, expectedArrival, expectedArrivalLocal |
| Detail | stops[] — only under view=detailed |
nextStop is the cheapest way to follow a driver's progress without asking for the whole stop list on every poll.
Change
The ExternalChangeDto — one entry in a plan's change journal, whether it was proposed and is waiting, or was applied outright.
| Group | Fields |
|---|---|
| What and where | id planId planName operation status source |
| Who and when | proposedBy with type, id, name · appliedBy createdAt proposedAt appliedAt rejectedAt expiresAt |
| Contents | diff (an ExternalActionPreviewDto) stale howToApply deduplicated |
How proposals are created and applied is covered in Proposed changes.
Enumerations
| Type | Values |
|---|---|
PlanStatus | CREATED DISPATCHING_IN_PROGRESS DISPATCHED DISPATCH_FAILED HAS_CHANGES APPROVED CANCELLED. The file-import statuses (IMPORT_*) only ever appear on plans created from the dashboard |
RouteStatus | OPTIMIZED IN_PROGRESS COMPLETED |
RouteStopStatus | PENDING DELIVERED NOT_DELIVERED |
RouteStopFailureReason | CUSTOMER_NOT_AVAILABLE WRONG_ADDRESS CUSTOMER_REFUSED BUSINESS_CLOSED ACCESS_RESTRICTED OTHER |
RouteStopErrorType | GEOCODE_FAILED CANT_VISIT_TIME_WINDOW CAPACITY_EXCEEDED OUTSIDE_TRANSIT_AREA MAX_JOBS_CONSTRAINT NO_DRIVER_WITH_ZONE_ACCESS ALLOWED_VEHICLE_CONSTRAINT |
DispatchStrategy | BALANCED_MIX BALANCE_BY_STOPS BALANCE_BY_TIME FASTEST_OVERALL |
StopPriority | EARLIEST AUTO LATEST |
StopSide | LEFT RIGHT ANY |
VehicleType | CAR VAN TRUCK BIKE SCOOTER |
ApiKeyScope | READ WRITE DISPATCH |
PlanChangeOperation | Proposable: DISPATCH APPROVE REVOKE CANCEL. Journal-only: OPTIMIZE ADD_STOPS UPDATE_STOP REMOVE_STOPS DUPLICATE |
PlanChangeStatus | APPLYING PROPOSED APPLIED REJECTED EXPIRED FAILED. Every row is written APPLYING before its operation runs, so an in-flight change reads APPLYING until it settles — on a DISPATCH that lasts the whole optimization. UNDONE exists in the enum but is reserved and never written |
WebhookEventType | plan.approved route.started route.completed stop.delivered stop.failed |
PlanStatus is a state machine, not a flat list — which transitions are legal is in Plan lifecycle & statuses.
What the API doesn't do
Listed plainly so you don't design an integration around a capability that isn't there. Some of these are covered by the dashboard; some are in progress.
| Limitation | What to do instead |
|---|---|
No lookup of a stop by your own identifier. metadata comes back as you sent it, but you can't query on it, and there is no GET /stops/{stopId} | Keep your own orderId → (planId, stopId) mapping. stopId arrives in the response to …/stops and in the plan.approved and route.started webhooks |
| Webhooks can't be configured over the API, and a failed delivery is not retried | Register your endpoint in the dashboard, and for anything critical run a periodic reconciliation against GET /plans/{planId} |
stop.delivered and stop.failed don't carry metadata | Match on the stopId from a snapshot you already hold |
| The address book can't be managed over the API — no create, update or delete. Entries are still created automatically from the stops you send | Edit and tidy the book in the dashboard; from an integration, send addresses on the stops themselves |
Drivers can't be created or deleted — only GET and a PATCH of operational fields | Onboard drivers in the dashboard |
Depots and zones are read-only, inside GET /team | Manage them in the dashboard |
| Proof of delivery — photos and signatures — is not exposed | The delivery status and the driver's note are available over the API; the files themselves live in the dashboard and the export |
| The public tracking link is not returned by any API object | Routerra sends the customer notification carrying that link itself — fill in the stop's email and phone |
| Live driver coordinates are not available | Follow progress through the route.* webhooks and nextStop in GET /plans/{planId} |
There is no pagination anywhere — only limit (plans 100, address book 50, changes 100) | Narrow the date range and walk day by day |
| A cancelled plan can't be restored over the API, even though it can be cancelled | Restore it in the dashboard, or duplicate the plan with …/duplicate |
| A proposal can't be rejected over the API, and an applied change can't be undone | Reject in the dashboard; roll back with ordinary revoke, approve or plan cancellation |
What's new
2026-09-07
- Key scopes
READ,WRITEandDISPATCH, with the new403 insufficient_scope. See API authentication & scopes. - Soft plan cancellation: the
CANCELLEDstatus,statusandrestorablein the response, the409 plan_cancellation_blockederror, and cancelled plans dropping out ofGET /plans. - A dashboard reserve inside the daily quota: the
optimizationQuota.availableForApifield. - Idempotency:
clientRequestIdon…/dispatch,…/stops,…/stops/{stopId}and…/stops/remove, thededuplicatedflag in their responses, and theclient_request_id_conflictandclient_request_in_progresserrors. - A new endpoint,
POST …/preview— see what an operation would do without changing anything. - Proposals:
POST …/changes,GET /changes,GET /changes/{changeId}andPOST /changes/{changeId}/apply; over MCP, aproposeparameter and an automatic hand-off to a dispatcher for a key withoutdispatch. - Recipient contacts
email,phone(E.164) andrecipientName— on stops, in the stop merge-patch, and in the address book. PATCH /drivers/{driverId}now refuses shift and vehicle changes while the driver has a route under way:409 driver_route_in_progress.- MCP: 18 tools —
apply_changeandlist_changesadded;approve_planmoved to the{applied, result, preview, change}envelope;autoRevokeremoved fromdispatch_plan(it stays in REST). - Webhooks documented — see Webhooks. The mechanism itself predates this release and did not change.
- The limitations section above was added.
Everything is additive except the three items in the box and the approve_plan envelope change on MCP. All 18 MCP tool names are frozen: nothing was renamed or removed.
Fixes shipped with the same release
metadataon a stop works again. Any request carrying that field —POST /optimize,POST …/stops,PUT …/stops/{stopId}, and the same tools over MCP — used to answer500. The blob is now accepted and returned as sent, nested objects and arrays included.- A client mistake no longer looks like a server failure. The wrong method on an existing path answers
405 method_not_allowedwith anAllowheader, and the wrongContent-Typeanswers415 unsupported_media_type. Both used to be a500. GET /changesvalidateslimit. Values outside 1–100 are refused with400 validation_error, as on every other list. Anything used to be accepted.- Concurrent requests no longer collide in the address book. Adding stops in parallel under the same new name could drop one request into a
500and leave the plan with no stops.
Corrections to this reference
These describe behaviour that never changed — earlier drafts of this documentation had it wrong. The first one may mean you are doing work you do not need to; the two after it change how an error handler should branch. All are worth a look even if nothing in your integration moved.
- The API geocodes addresses, and always has. The retired
optimize-apiarticle said the opposite — thatlatitudeandlongitudewere required, that a stop carrying only anaddresswas rejected with400 empty_coordinates, and that you had to geocode on your side before calling. None of that was true of the implementation. Send anaddressand the server resolves it; a resolution failure does not even fail the request, it comes back per-stop ingeocodeFailedStops. If you built a geocoding step to satisfy the old article you can retire it, and400 empty_coordinatesis not a case an external caller needs to handle — see How a stop is located. concurrent_optimization_limitarrives as429, not409. The implementation always returned429. A handler that looks for this code in its409branch will never find it.- An MCP call that starts an optimization spends two rate-limit tokens, not one. The URI-based filter puts
/mcpin the general tier, butoptimize_plan,dispatch_planand applying aDISPATCHproposal each take an optimize-tier token by hand on top of it. When that hand-taken token is gone the answer is429 too_many_requests, notrate_limit_exceeded— see API limits & quotas. 400 bad_requestis documented. It always existed; today it comes from an unknown?view=value. Its overlap withvalidation_erroris called out explicitly in API error reference.- Checking the age of the
tvalue in a webhook signature is entirely the receiver's job. Routerra is the sender and does not perform that check. See Webhooks.
Other clarifications in this pass: the rate-limit 429 carries rate_limit_exceeded, not too_many_requests; a completed route blocks cancellation just as a running one does; a team with no subscription record is never given a 402; the request limits are token buckets refilling continuously rather than fixed windows; and empty members of a response envelope are omitted rather than sent empty.
What's next
- API error reference — every
code, and the formats these fields use - Plan lifecycle & statuses —
PlanStatusas a state machine - Proposed changes — where
ExternalChangeDtocomes from - MCP connector — the 18 tools over the same objects