Routes
The drive order for a crew's day. Placing work decides WHAT a crew does; this decides the order they do it in, and it is the last thing between a scheduled day and a truck that is not doubling back.
https://app.treeinventory.ai/api/v1/routes/{crewId}/{date}Get a crew's day in drive order
The order the crew should drive, with the legs and the encoded polyline when the optimizer has produced them. orderedScheduleIds is ALWAYS safe to use whatever the status says: when nothing has been optimized it falls back to the order the work was placed in, so a client never has to handle an empty answer. Read controlVersion here before setting an order by hand.
Scope: routing:read
Path parameters
| Field | Type | Notes |
|---|---|---|
| crewIdrequired | uuid | The crew, from GET /crews. |
| daterequired | date | The day, YYYY-MM-DD. |
data
| Field | Type | Notes |
|---|---|---|
| crewId | uuid | |
| date | string | YYYY-MM-DD. |
| status | "not_configured" | "not_needed" | "pending" | "optimizing" | "ready" | "needs_attention" | "failed" | "stale" | "frozen" | ready is the only status whose order came from the optimizer. pending and optimizing mean ask again shortly. stale means the day changed under a plan that is still correct for the old day. frozen means the day is close enough that the order is deliberately held. not_configured means the org has no origin set, and no amount of retrying will change that. Whatever the status, orderedScheduleIds is always safe to drive: it falls back to the order the placements were made in. |
| optimizedAt | string or null | |
| distanceMeters | number or null | |
| driveDurationSeconds | number or null | |
| orderMode | "automatic" | "manual" | manual means somebody pinned the order and the optimizer will not move it until POST .../automatic hands control back. |
| orderSource | "optimized" | "manual" | "schedule" | Where the order in orderedScheduleIds actually came from. schedule means nobody optimized anything and this is simply the order the work was placed in. |
| controlVersion | integer | Send this back as expectedVersion when setting an order by hand. It is what stops two dispatchers reordering the same day and one of them silently winning. |
| orderedScheduleIds | uuid[] | The drive order. Always populated, whatever the status. |
| legs | object[] | |
| geometry | object or null | Google's encoded polyline for the whole route, when there is one. |
| origin | object or null | Where the day starts and, unless configured otherwise, ends. |
| issues | object[] | Why the order is not optimized. missing_coordinate names the placement whose site was never geocoded, which is the one an operator can actually fix. |
data → legs
| Field | Type | Notes |
|---|---|---|
| from | object | |
| to | object | |
| distanceMeters | number | |
| driveDurationSeconds | number |
data → issues
| Field | Type | Notes |
|---|---|---|
| code | "missing_coordinate" | "multi_location" | "provider_unavailable" | |
| scheduleId | uuid or null |
Example response 200
{
"success": true,
"message": "Route retrieved",
"data": {
"crewId": "d2f4a601-88b3-4c17-9e5a-3b7f0c1d2e94",
"date": "2026-09-08",
"status": "ready",
"optimizedAt": "2026-09-07T21:04:11.882Z",
"distanceMeters": 41230,
"driveDurationSeconds": 3480,
"orderMode": "automatic",
"orderSource": "optimized",
"controlVersion": 3,
"orderedScheduleIds": [
"a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e"
],
"legs": [
{
"from": {
"type": "origin",
"label": "Yard",
"scheduleId": null
},
"to": {
"type": "stop",
"label": null,
"scheduleId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
},
"distanceMeters": 14820,
"driveDurationSeconds": 1140
}
],
"geometry": {
"encodedPolyline": "_p~iF~ps|U_ulLnnqC",
"precision": 5
},
"origin": {
"label": "Yard",
"lat": 39.1653,
"lng": -86.5264
},
"issues": []
}
}Failures
401Missing, malformed, revoked or expired credential. TheWWW-Authenticateheader names the scheme, which isApiKey. Carries noRateLimit-*headers: the credential was never resolved, so there is no bucket to report.403The key is valid but does not carry the scope this operation requires. The message names the scope, so you can ask for exactly the grant you need. Carries noRateLimit-*headers: a request refused for scope is not counted against your limit.404No such record in your organization. A record belonging to somebody else is indistinguishable from one that does not exist.422Validation failed. Every bad field is named, at once.429Rate limit exceeded.500Something failed on our side. Retry with the SAME Idempotency-Key; a 5xx is the one status where retrying is correct.
https://app.treeinventory.ai/api/v1/routes/{crewId}/{date}/optimizeAsk for the day to be re-optimized
QUEUES an optimization; it does not compute one. The work happens against an external provider outside this request, so the answer is pending and you poll GET /routes/{crewId}/{date} until the status is ready. Placing work already queues this automatically, so call it when you want a day recomputed for some other reason. Each call is a billed provider request, which is why it sits behind its own scope.
Scope: routing:write
Path parameters
| Field | Type | Notes |
|---|---|---|
| crewIdrequired | uuid | The crew. |
| daterequired | date | The day, YYYY-MM-DD. |
Headers
| Field | Type | Notes |
|---|---|---|
| Idempotency-Key | uuid | A UUID you mint per logical action and REUSE on every retry of it. Replay returns the original response and creates nothing. Omit it and a retry creates a second record. |
data
| Field | Type | Notes |
|---|---|---|
| status | "pending" | Queued, not computed. Optimization is a call to an external provider and it does not happen inside this request. Poll GET /routes/{crewId}/{date} until the status is ready. |
Example response 200
{
"success": true,
"message": "Optimization requested",
"data": {
"status": "pending"
}
}Failures
401Missing, malformed, revoked or expired credential. TheWWW-Authenticateheader names the scheme, which isApiKey. Carries noRateLimit-*headers: the credential was never resolved, so there is no bucket to report.403The key is valid but does not carry the scope this operation requires. The message names the scope, so you can ask for exactly the grant you need. Carries noRateLimit-*headers: a request refused for scope is not counted against your limit.404No such record in your organization. A record belonging to somebody else is indistinguishable from one that does not exist.409Idempotency conflict.idempotency_key_reusedmeans thisIdempotency-Keywas already used with a DIFFERENT request body: mint a new key for a new request, or resend the original body to replay.command_in_progressmeans an identical request is still in flight; waitRetry-Afterseconds and retry.422Validation failed. Every bad field is named, at once.429Rate limit exceeded.500Something failed on our side. Retry with the SAME Idempotency-Key; a 5xx is the one status where retrying is correct.
https://app.treeinventory.ai/api/v1/routes/{crewId}/{date}/orderSet the drive order by hand
Pins the order and switches the day to manual, so the optimizer stops moving it. Send EVERY placement on the day: a partial list is refused, because a day reordered halfway is a day nobody can read. expectedVersion is the controlVersion you read; a stale one answers 409 instead of overwriting whatever changed underneath you.
Scope: routing:write
Path parameters
| Field | Type | Notes |
|---|---|---|
| crewIdrequired | uuid | The crew. |
| daterequired | date | The day, YYYY-MM-DD. |
Headers
| Field | Type | Notes |
|---|---|---|
| Idempotency-Key | uuid | A UUID you mint per logical action and REUSE on every retry of it. Replay returns the original response and creates nothing. Omit it and a retry creates a second record. |
Request body
| Field | Type | Notes |
|---|---|---|
| expectedVersionrequired | integer | The controlVersion you read. A stale one is refused with 409 rather than overwriting whatever changed in between. |
| orderedScheduleIdsrequired | uuid[] | Every placement on the day, in the order you want them driven. A partial list is refused: the day is reordered as a whole. |
Example request
{
"expectedVersion": 3,
"orderedScheduleIds": [
"b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
"a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
]
}data
| Field | Type | Notes |
|---|---|---|
| orderMode | "automatic" | "manual" | |
| controlVersion | integer | The NEW version. Read this one back for your next write. |
| orderedScheduleIds | uuid[] |
Example response 200
{
"success": true,
"message": "Order set",
"data": {
"orderMode": "manual",
"controlVersion": 4,
"orderedScheduleIds": [
"b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
"a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
]
}
}Failures
401Missing, malformed, revoked or expired credential. TheWWW-Authenticateheader names the scheme, which isApiKey. Carries noRateLimit-*headers: the credential was never resolved, so there is no bucket to report.403The key is valid but does not carry the scope this operation requires. The message names the scope, so you can ask for exactly the grant you need. Carries noRateLimit-*headers: a request refused for scope is not counted against your limit.404No such record in your organization. A record belonging to somebody else is indistinguishable from one that does not exist.409Idempotency conflict.idempotency_key_reusedmeans thisIdempotency-Keywas already used with a DIFFERENT request body: mint a new key for a new request, or resend the original body to replay.command_in_progressmeans an identical request is still in flight; waitRetry-Afterseconds and retry.422Validation failed. Every bad field is named, at once.429Rate limit exceeded.500Something failed on our side. Retry with the SAME Idempotency-Key; a 5xx is the one status where retrying is correct.
https://app.treeinventory.ai/api/v1/routes/{crewId}/{date}/automaticHand the day back to the optimizer
Undoes a manual order: the day returns to automatic and the optimizer is free to reorder it again. The order does not change in this call, only who is allowed to change it next.
Scope: routing:write
Path parameters
| Field | Type | Notes |
|---|---|---|
| crewIdrequired | uuid | The crew. |
| daterequired | date | The day, YYYY-MM-DD. |
Headers
| Field | Type | Notes |
|---|---|---|
| Idempotency-Key | uuid | A UUID you mint per logical action and REUSE on every retry of it. Replay returns the original response and creates nothing. Omit it and a retry creates a second record. |
Request body
| Field | Type | Notes |
|---|---|---|
| expectedVersionrequired | integer | The controlVersion you read. |
Example request
{
"expectedVersion": 4
}data
| Field | Type | Notes |
|---|---|---|
| orderMode | "automatic" | "manual" | |
| controlVersion | integer | The NEW version. Read this one back for your next write. |
| orderedScheduleIds | uuid[] |
Example response 200
{
"success": true,
"message": "Order handed back to the optimizer",
"data": {
"orderMode": "automatic",
"controlVersion": 5,
"orderedScheduleIds": [
"b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
"a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
]
}
}Failures
401Missing, malformed, revoked or expired credential. TheWWW-Authenticateheader names the scheme, which isApiKey. Carries noRateLimit-*headers: the credential was never resolved, so there is no bucket to report.403The key is valid but does not carry the scope this operation requires. The message names the scope, so you can ask for exactly the grant you need. Carries noRateLimit-*headers: a request refused for scope is not counted against your limit.404No such record in your organization. A record belonging to somebody else is indistinguishable from one that does not exist.409Idempotency conflict.idempotency_key_reusedmeans thisIdempotency-Keywas already used with a DIFFERENT request body: mint a new key for a new request, or resend the original body to replay.command_in_progressmeans an identical request is still in flight; waitRetry-Afterseconds and retry.422Validation failed. Every bad field is named, at once.429Rate limit exceeded.500Something failed on our side. Retry with the SAME Idempotency-Key; a 5xx is the one status where retrying is correct.