Optimize API reference
The full request/response reference for the Routerra Teams Optimize API — authenticate with an API key, send stops, poll for optimized routes.
The Optimize API lets your own systems run the same optimization engine the web app uses: send stops and a driver selection, get back optimized routes. It's three endpoints — one to submit work, one to poll for the result, one to look up your drivers.
All requests go to the production base URL:
https://teams-api.routerra.io/api/v1
Authentication
Every request needs an API key in the X-API-Key header. Keys start with rtk_ and are created under Settings → Developer → API keys — see API keys for the create/revoke flow. A key is scoped to the team that created it: every plan, stop, and driver you touch through the API belongs to that team.
curl "https://teams-api.routerra.io/api/v1/external/drivers" \
-H "X-API-Key: rtk_your_key_here"
A missing header fails with 401 {"code": "invalid_api_key", "message": "Missing X-API-Key header"}; a wrong, disabled, or expired key fails with the same code and "Invalid, disabled, or expired API key."
The endpoints
| Method | Path | What it does |
|---|---|---|
POST | /external/optimize | Create a plan from your stops and start optimization |
GET | /external/plans/{planId} | Poll optimization status and fetch the routes |
GET | /external/drivers | List your team's drivers and their IDs |
POST /external/optimize
One call does the whole job: it creates a plan, adds your stops to it, and starts optimization across the drivers you selected. Optimization runs asynchronously — the response comes back immediately with a planId you poll.
curl -X POST "https://teams-api.routerra.io/api/v1/external/optimize" \
-H "X-API-Key: rtk_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"name": "Tuesday deliveries",
"date": "2026-07-28",
"stops": [
{
"name": "Customer A",
"address": "123 Main St, Chicago, IL",
"latitude": 41.8781,
"longitude": -87.6298
},
{
"name": "Customer B",
"address": "456 Oak Ave, Chicago, IL",
"latitude": 41.92,
"longitude": -87.65,
"serviceTime": 600,
"arrivalRangeFrom": "09:00",
"arrivalRangeTo": "12:00",
"metadata": { "orderId": "ORD-123" }
}
],
"driverIds": [101, 102],
"strategy": "BALANCED_MIX"
}'
Request fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | no | "API Plan {date}" | Plan name, max 255 characters |
date | string | no | today | Plan date, yyyy-MM-dd |
stops | Stop[] | yes | — | The stops to optimize, at least 1 |
driverIds | number[] | no | all team drivers | Which drivers to route — IDs from GET /external/drivers |
strategy | string | no | BALANCED_MIX | Distribution strategy, see below |
Stop fields
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Stop label, max 255 characters |
latitude | number | yes | WGS84 latitude, decimal degrees |
longitude | number | yes | WGS84 longitude, decimal degrees |
address | string | no | Human-readable address — stored and shown to the driver; routing uses the coordinates |
serviceTime | number | no | Time the driver spends at the stop, in seconds. Omit to use your team's default service time |
arrivalRangeFrom | string | no | Earliest allowed arrival, HH:mm |
arrivalRangeTo | string | no | Latest allowed arrival, HH:mm |
priority | string | no | Arrival priority: AUTO, EARLIEST, or LATEST — bias toward serving the stop early or late (defaults to AUTO) |
stopSide | string | no | Curbside parking: ANY, LEFT, or RIGHT (defaults to ANY) |
note | string | no | Free-text note for the driver |
load | number | no | Capacity units this stop consumes, weighed against each driver's vehicle capacity |
metadata | object | no | Arbitrary JSON stored with the stop and returned as-is — use it to carry your own order or customer IDs |
Distribution strategies
The same four strategies as the Assign drivers & optimize dialog — see Optimize routes for when to pick which:
| API value | In the app |
|---|---|
BALANCE_BY_STOPS | Balance by Stops |
BALANCE_BY_TIME | Balance by Time |
FASTEST_OVERALL | Fastest Overall |
BALANCED_MIX | Balanced Mix |
Response — 202 Accepted
{
"planId": 4567,
"status": "DISPATCHING_IN_PROGRESS",
"stopsCount": 2,
"createdAt": "2026-07-28T08:30:00Z"
}
| Field | Type | Description |
|---|---|---|
planId | number | The plan's ID — use it to poll GET /external/plans/{planId} |
status | string | Always DISPATCHING_IN_PROGRESS on submit |
stopsCount | number | How many stops were accepted |
createdAt | string | Plan creation time, ISO 8601 |
GET /external/plans/{planId}
Poll this endpoint until status leaves DISPATCHING_IN_PROGRESS. While optimization is running you get the plan header only; once it finishes, the response includes every route with its stops in visit order.
curl "https://teams-api.routerra.io/api/v1/external/plans/4567" \
-H "X-API-Key: rtk_your_key_here"
Fields that are null are omitted from the response entirely.
Plan fields
| Field | Type | Description |
|---|---|---|
id | number | Plan ID |
name | string | Plan name |
date | string | Plan date, yyyy-MM-dd |
status | string | See statuses below |
stopsCount | number | Total stops in the plan |
routesCount | number | Number of routes built — present after optimization |
totalTimeMinutes | number | Sum of all route times, in minutes — present after optimization |
totalDistanceMeters | number | Sum of all route distances, in meters — present after optimization |
createdAt | string | ISO 8601 |
updatedAt | string | ISO 8601 |
routes | Route[] | The optimized routes — present only when status is DISPATCHED or DISPATCH_FAILED |
unassignedStops | Stop[] | Stops the optimizer couldn't place — present only when there are any |
Plan statuses
| Status | Meaning |
|---|---|
DISPATCHING_IN_PROGRESS | Optimization is running — keep polling |
DISPATCHED | Done. routes holds the result; check unassignedStops for anything left out |
DISPATCH_FAILED | Optimization failed. Stops in unassignedStops carry an errorType explaining why |
Route fields
| Field | Type | Description |
|---|---|---|
id | number | Route ID |
driverId | number | Assigned driver |
driverName | string | Assigned driver's name |
distance | number | Route distance, in meters |
time | number | Route duration, in minutes |
stopsCount | number | Stops on this route |
startLocation | Location | Where the route starts |
finishLocation | Location | Where the route ends — omitted if the driver has no finish location |
stops | Stop[] | The stops in optimized visit order |
A Location is { "address", "latitude", "longitude" }.
Stop fields (response)
| Field | Type | Description |
|---|---|---|
id | number | Stop ID |
name | string | Stop label |
address | string | Address as submitted |
latitude, longitude | number | Coordinates as submitted |
position | number | Visit order within the route, starting at 1 |
expectedArrival | string | Predicted arrival, ISO 8601 with the route's local UTC offset |
driveTime | number | Drive time from the previous point, in minutes |
driveDistance | number | Drive distance from the previous point, in meters |
waitTime | number | Idle time before an arrival window opens, in minutes — omitted when under 5 minutes |
serviceTime | number | Service time in seconds — omitted when the team default applies |
status | string | Delivery status: PENDING, DELIVERED, or NOT_DELIVERED |
errorType | string | Why the stop couldn't be placed — only on unassigned stops |
metadata | object | Your metadata, returned exactly as submitted |
Stop error types
When the optimizer can't fit a stop, it lands in unassignedStops with one of:
errorType | Meaning |
|---|---|
CANT_VISIT_TIME_WINDOW | No driver can reach the stop inside its arrival window |
CAPACITY_EXCEEDED | The stop's load doesn't fit any driver's vehicle capacity |
NO_DRIVER_WITH_ZONE_ACCESS | The stop sits in a zone none of the selected drivers may enter |
MAX_JOBS_CONSTRAINT | Every driver already carries their maximum number of stops |
ALLOWED_VEHICLE_CONSTRAINT | The stop's zone restricts vehicle types and no allowed vehicle was selected |
OUTSIDE_TRANSIT_AREA | The coordinates fall outside the routable road network |
GEOCODE_FAILED | The stop has no usable coordinates — seen on stops imported from files, not on API-created stops |
GET /external/drivers
Lists all drivers in your team, with the IDs you pass as driverIds when optimizing. Drivers themselves are managed in the web app — see Manage drivers.
curl "https://teams-api.routerra.io/api/v1/external/drivers" \
-H "X-API-Key: rtk_your_key_here"
[
{
"id": 101,
"name": "John Smith",
"vehicleType": "VAN",
"vehicleCapacity": 100,
"maxStops": 30,
"shiftStartTime": "08:00",
"shiftEndTime": "17:00",
"startLocation": {
"address": "456 Depot Rd, Chicago, IL",
"latitude": 41.85,
"longitude": -87.65
},
"liveRoadData": true,
"avoidTolls": false,
"avoidHighways": false
}
]
| Field | Type | Description |
|---|---|---|
id | number | Driver ID — use in driverIds |
name | string | Driver name |
vehicleType | string | CAR, TRUCK, VAN, BIKE, or SCOOTER |
vehicleCapacity | number | Capacity in load units |
maxStops | number | Maximum stops per route |
shiftStartTime, shiftEndTime | string | Working hours, HH:mm |
startLocation | Location | Where routes start |
finishLocation | Location | Where routes end — omitted if not set |
liveRoadData | boolean | Whether routing uses live traffic |
avoidTolls | boolean | Routing avoids toll roads |
avoidHighways | boolean | Routing avoids highways |
As everywhere in the API, null fields are omitted — a driver without a shift simply has no shiftStartTime.
Errors
Every error is JSON with a code and a human-readable message:
{ "code": "validation_error", "message": "stops[0].name: must not be blank" }
Validation failures list each offending field in the message, separated by semicolons.
| HTTP | code | When |
|---|---|---|
| 400 | validation_error | A request field fails validation (blank stop name, empty stops, …) |
| 400 | empty_coordinates | A stop is missing latitude/longitude |
| 400 | malformed_request | The body isn't valid JSON for this endpoint |
| 400 | invalid_driver_selection | A driverIds entry doesn't exist or belongs to another team |
| 401 | invalid_api_key | Missing, wrong, disabled, or expired key |
| 402 | subscription_required | The team has no active subscription |
| 404 | plan_not_found | The plan doesn't exist or belongs to another team |
| 409 | stop_limit_exceeded | The plan would exceed your team's stops-per-plan limit |
| 429 | too_many_requests | Optimization quota exhausted — see limits below |
| 500 | internal_error | Something unexpected — retry, and contact support if it persists |
Limits
Two per-team limits apply, both checked before any work starts:
- Optimization runs per day — each successful
POST /external/optimizeconsumes one run from the same daily quota the web app uses (default 30 per day). A run whose optimization fails is refunded. Exceeding it returns429 too_many_requests. - Stops per plan — one plan holds at most your team's stop limit (default 200 stops). Exceeding it returns
409 stop_limit_exceeded.
What's next
- API keys — create and manage the keys this API authenticates with
- Webhooks — get pushed a signed event when routes start, complete, or fail instead of polling
- Optimize routes — what the optimizer respects, in product terms