Schedule
Placing accepted work on a crew and a day, and reading back what is placed. This is the step between a quote and a truck leaving the yard.
https://app.treeinventory.ai/api/v1/scheduleList placed work
What is on the board, ordered by day and then by drive order within each crew's day. needsAttention is live work whose last placement is already in the past: it was never done and it is no longer on anybody's day, so it would otherwise vanish quietly. This is the read that makes a placement checkable.
Scope: schedule:read
Query parameters
| Field | Type | Notes |
|---|---|---|
| from | string | Placements on or after this day. Defaults to today, UTC. |
data
| Field | Type | Notes |
|---|---|---|
| upcoming | ScheduledWork[] | |
| needsAttention | ScheduledWork[] | Live work whose LAST placement is in the past. It has not been done and it is no longer on anybody's day, so it would otherwise vanish quietly. |
data → upcoming
| Field | Type | Notes |
|---|---|---|
| scheduleId | uuid | The placement, not the estimate. |
| estimateId | uuid | |
| estimateNumber | string | Formatted, e.g. "JOB-0062". |
| estimateStatus | string | |
| scheduledDate | string | YYYY-MM-DD, the crew's local day. |
| sortOrder | integer | Position within that crew's day, ascending. This is the drive order. |
| note | string or null | |
| siteId | uuid | |
| siteLabel | string | |
| latitude | number or null | |
| longitude | number or null | |
| totalCents | integer | |
| crew | object or null | null means placed on the day with no crew yet ("pencilled"). |
data → needsAttention
| Field | Type | Notes |
|---|---|---|
| scheduleId | uuid | The placement, not the estimate. |
| estimateId | uuid | |
| estimateNumber | string | Formatted, e.g. "JOB-0062". |
| estimateStatus | string | |
| scheduledDate | string | YYYY-MM-DD, the crew's local day. |
| sortOrder | integer | Position within that crew's day, ascending. This is the drive order. |
| note | string or null | |
| siteId | uuid | |
| siteLabel | string | |
| latitude | number or null | |
| longitude | number or null | |
| totalCents | integer | |
| crew | object or null | null means placed on the day with no crew yet ("pencilled"). |
Example response 200
{
"success": true,
"message": "Scheduled work retrieved",
"data": {
"upcoming": [
{
"scheduleId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"estimateId": "e5d4c3b2-a190-4877-b6e5-d4c3b2a19087",
"estimateNumber": "JOB-0062",
"estimateStatus": "scheduled",
"scheduledDate": "2026-09-08",
"sortOrder": 1000,
"note": null,
"siteId": "3a7c9e21-5b48-4f0a-9d33-71c6ee204b18",
"siteLabel": "3760 W Delap Rd",
"latitude": 39.1912,
"longitude": -86.5847,
"totalCents": 122500,
"crew": {
"id": "d2f4a601-88b3-4c17-9e5a-3b7f0c1d2e94",
"name": "Crew A",
"color": "#2f855a",
"isArchived": false
}
}
],
"needsAttention": []
}
}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/schedulePlace accepted work on a crew and a day
Only accepted work can be placed, which is why POST /estimates/{estimateId}/accept comes first. Send the ids IN DRIVE ORDER: position on the day is assigned in the order given, so the array is the route and no separate ordering call exists. The answer is 200 with a per-estimate report even when some failed, because a batch is a gesture rather than an atomic unit and the ones that landed stay landed. Send an Idempotency-Key: each estimate derives a stable id from it, so retrying the same batch replays estimate by estimate and places nothing twice. Note that a replayed estimate is currently reported as placed rather than skipped, so read the board with GET /schedule if you need to know which of the two happened; the placement itself is not duplicated either way.
Scope: schedule:write
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 |
|---|---|---|
| estimateIdsrequired | uuid[] | IN THE ORDER THEY SHOULD BE DRIVEN. Position on the day is assigned in the order given, so this array is the route. Duplicates are collapsed. |
| crewIdrequired | uuid or null | The crew to place them on, or null to pencil the day in without one. Read GET /crews for the id. |
| scheduledDaterequired | string | |
| note | string or null |
Example request
{
"estimateIds": [
"e5d4c3b2-a190-4877-b6e5-d4c3b2a19087"
],
"crewId": "d2f4a601-88b3-4c17-9e5a-3b7f0c1d2e94",
"scheduledDate": "2026-09-08",
"note": "Ellettsville cluster, north to south"
}Each item in data.items
| Field | Type | Notes |
|---|---|---|
| estimateId | uuid | |
| status | "placed" | "skipped" | "failed" | skipped means it was already on that day: a no-op, not a failure. failed means this one did not land and error says why. |
| error | string or null |
Example response 200
{
"success": true,
"message": "Work scheduled",
"data": {
"placedCount": 1,
"failedCount": 0,
"items": [
{
"estimateId": "e5d4c3b2-a190-4877-b6e5-d4c3b2a19087",
"status": "placed",
"error": null
}
]
}
}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.