{"openapi":"3.1.0","info":{"title":"Tree Inventory AI API","version":"1.0.0","summary":"Tree inventory, sites and estimates, for programs.","description":"This API is built for agents first. Everything an integration needs is\nin this document: one response shape, status codes that mean what they\nsay, rate-limit headers on every counted response, and an example on\nevery operation. The two failures decided before counting, a 401 for\na credential we could not resolve and a 403 for a missing scope,\ncarry no `RateLimit-*` headers and do not consume quota.\n\n**Authentication.** Send `Authorization: ApiKey <your key>` on every\nrequest. Not `Bearer`: a bearer token here is a session token, and\nthis surface deliberately does not accept one.\n\n**One envelope, always.** Success and failure both return\n`{success, message, data}`. A client that parses the success shape\nwill not crash on the first failure it meets.\n\n**Branch on `data.code`, not on `message`.** The codes are stable; the\nprose is not.\n\n**Retries.** Retry a 5xx and a 429. Never retry a 4xx: it will fail\nidentically forever. When you retry a POST, send the same\n`Idempotency-Key` you sent the first time.\n\n**Tenancy.** Your key carries its organization. There is no `orgId`\nfield anywhere in this API, and a record belonging to another\norganization answers 404, exactly as one that does not exist.","contact":{"name":"Tree Inventory AI","url":"https://treeinventory.ai"}},"servers":[{"url":"https://app.treeinventory.ai/api/v1","description":"Production"}],"tags":[{"name":"Customers","description":"The people and organizations you work for. Everything else hangs off a customer: a site belongs to one, and an estimate is written for the customer who owns the site it prices."},{"name":"Sites","description":"Job sites, one per property. The domain calls these addresses internally; the API calls them sites, because that is what an arborist means."},{"name":"Trees","description":"Individual trees captured in the field, with their measurements and the confidence attached to each one. Read-only: capture happens in the mobile app, where the camera is."},{"name":"Notes","description":"What the arborist said on the walk. Dictated at the tree, or the conversation with the customer pasted against the site. The price and the constraints are usually in here rather than in the inventory."},{"name":"Catalog","description":"The products and services the org sells. Read this before writing an estimate: every line references a catalog item id."},{"name":"Estimates","description":"Quotes. This is the only write that creates real money-shaped work, so it is the one to send an Idempotency-Key with."},{"name":"Invoices","description":"Billing a completed job, and seeing what is still owed. This is the only part of the API that touches money, and the only estimate it will bill is one a person has already marked complete."},{"name":"Crews","description":"The teams work gets placed on. Read-only here: a crew is made of real org users, and adding one is an account decision rather than an integration one."},{"name":"Schedule","description":"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."},{"name":"Routes","description":"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."},{"name":"Reports","description":"A site's mapped inventory as GeoJSON: the same facts a PDF report shows, in a form a program can read."}],"security":[{"ApiKey":[]}],"components":{"headers":{"RateLimit-Limit":{"description":"Requests allowed per hour for this key.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in the current window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"SECONDS until the window resets. A delta, not an epoch, and the same unit as Retry-After.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}},"WWW-Authenticate":{"description":"Always `ApiKey realm=\"treeinventory.ai\"`.","schema":{"type":"string"}}},"schemas":{"Error":{"description":"Every failure, in the same shape as every success.","type":"object","properties":{"success":{"type":"boolean","const":false},"message":{"description":"Human-readable and NOT stable. Do not branch on it.","type":"string"},"data":{"anyOf":[{"type":"object","properties":{"code":{"description":"Stable machine code. Branch on this, never on `message`. 401 is `unauthorized`; 403 `forbidden`; 404 `not_found`; 409 `idempotency_key_reused` when the same Idempotency-Key arrives with a different body, `command_in_progress` when an identical one is still in flight, `conflict` otherwise; 422 `validation_failed`; 429 `rate_limited`; 5xx `internal_error`.","type":"string","enum":["unauthorized","forbidden","not_found","conflict","idempotency_key_reused","command_in_progress","validation_failed","rate_limited","internal_error"]}},"additionalProperties":{}},{"type":"null"}]}},"required":["success","message","data"],"additionalProperties":false},"ValidationError":{"description":"422. The fields are named so an agent can repair and retry.","type":"object","properties":{"success":{"type":"boolean","const":false},"message":{"type":"string","const":"Validation failed"},"data":{"type":"object","properties":{"errors":{"description":"Every problem at once, so one retry can fix all of them.","type":"array","items":{"type":"object","properties":{"field":{"description":"Dotted path into the request body (\"customer.email\"), a header name, or \"(body)\" when the body itself is unusable.","type":"string"},"problem":{"description":"What is wrong with that field.","type":"string"}},"required":["field","problem"],"additionalProperties":false}}},"required":["errors"],"additionalProperties":false}},"required":["success","message","data"],"additionalProperties":false},"Customer":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"name":{"type":"string"},"email":{"anyOf":[{"type":"string"},{"type":"null"}]},"phone":{"anyOf":[{"type":"string"},{"type":"null"}]},"createdAt":{"description":"ISO-8601, UTC.","type":"string"},"updatedAt":{"description":"ISO-8601, UTC.","type":"string"}},"required":["id","name","email","phone","createdAt","updatedAt"],"additionalProperties":false,"id":"Customer"},"Site":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"customerId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"label":{"description":"Optional nickname (\"Back lot\").","anyOf":[{"type":"string"},{"type":"null"}]},"street":{"type":"string"},"city":{"type":"string"},"state":{"type":"string"},"zip":{"type":"string"},"country":{"type":"string"},"latitude":{"description":"Null until the site has been geocoded or located.","anyOf":[{"type":"number"},{"type":"null"}]},"longitude":{"anyOf":[{"type":"number"},{"type":"null"}]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","customerId","label","street","city","state","zip","country","latitude","longitude","createdAt","updatedAt"],"additionalProperties":false,"id":"Site"},"Tree":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"siteId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"commonName":{"description":"Null until identified.","anyOf":[{"type":"string"},{"type":"null"}]},"scientificName":{"anyOf":[{"type":"string"},{"type":"null"}]},"speciesConfidence":{"description":"0-1. Published beside the value it qualifies: a measurement without its confidence reads as certainty nobody has.","anyOf":[{"type":"number"},{"type":"null"}]},"speciesSource":{"description":"Who decided the species: `ai`, `user-confirmed`, `user-corrected` or `live-confirmed`. READ THIS BEFORE YOU PRINT A SPECIES NAME ON ANYTHING A CUSTOMER SEES. Vision species identification is right about a third of the time on its own, so `ai` with a low `speciesConfidence` is a guess, and an arborist who sees their own guess quoted back as fact stops trusting the whole quote. Anything but `ai` means a person looked at the tree and said so.","type":"string"},"dbhInches":{"description":"Diameter at breast height.","anyOf":[{"type":"number"},{"type":"null"}]},"dbhConfidence":{"description":"0-1 confidence in dbhInches. Null means it was not estimated.","anyOf":[{"type":"number"},{"type":"null"}]},"heightFeet":{"anyOf":[{"type":"number"},{"type":"null"}]},"heightConfidence":{"description":"0-1 confidence in heightFeet.","anyOf":[{"type":"number"},{"type":"null"}]},"canopySpreadFeet":{"anyOf":[{"type":"number"},{"type":"null"}]},"canopyConfidence":{"description":"0-1 confidence in canopySpreadFeet.","anyOf":[{"type":"number"},{"type":"null"}]},"trunkCount":{"anyOf":[{"type":"number"},{"type":"null"}]},"quantity":{"description":"How many physical trees this record stands for. Usually 1.","type":"number"},"healthCondition":{"description":"good | fair | poor | dead | hazardous.","anyOf":[{"type":"string"},{"type":"null"}]},"hazardRating":{"anyOf":[{"type":"number"},{"type":"null"}]},"riskAssessment":{"anyOf":[{"type":"string"},{"type":"null"}]},"defects":{"type":"array","items":{"type":"string"}},"observations":{"type":"array","items":{"type":"string"}},"recommendations":{"type":"array","items":{"type":"string"}},"tags":{"type":"array","items":{"type":"string"}},"latitude":{"anyOf":[{"type":"number"},{"type":"null"}]},"longitude":{"anyOf":[{"type":"number"},{"type":"null"}]},"gpsAccuracyMeters":{"anyOf":[{"type":"number"},{"type":"null"}]},"capturedAt":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","siteId","commonName","scientificName","speciesConfidence","speciesSource","dbhInches","dbhConfidence","heightFeet","heightConfidence","canopySpreadFeet","canopyConfidence","trunkCount","quantity","healthCondition","hazardRating","riskAssessment","defects","observations","recommendations","tags","latitude","longitude","gpsAccuracyMeters","capturedAt","createdAt","updatedAt"],"additionalProperties":false,"id":"Tree"},"CatalogItem":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"type":{"description":"`service`, `product` or `bundle`. A BUNDLE IS A GROUPING AND CARRIES A ZERO PRICE: it exists so a multi-visit program can be sold as one thing, and putting one on an estimate line puts a free line on somebody's quote. Quote the items inside it instead.","type":"string"},"name":{"type":"string"},"description":{"anyOf":[{"type":"string"},{"type":"null"}]},"category":{"type":"string"},"unitPriceCents":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"active":{"type":"boolean"},"workTypeHint":{"description":"An auto-match HINT, never a constraint on what this item may price. Read it as a constraint and you will build a matcher that silently drops work.","anyOf":[{"type":"string"},{"type":"null"}]},"dbhMinIn":{"description":"The smallest trunk diameter this item prices, in inches, inclusive. Null means the item is not sized by diameter at all.","anyOf":[{"type":"number"},{"type":"null"}]},"dbhMaxIn":{"description":"The largest trunk diameter this item prices, in inches, inclusive. Null on the top band means no upper limit.\n\nPRICE FROM THESE, NOT FROM THE ITEM NAME. Tree work is catalogued by trunk diameter rather than by operation, so there is usually no item called `crown clean`: there is a pruning item for each band. Match `workTypeHint` first, then find the band a tree's `dbhInches` falls in. Parsing the band out of the name works until somebody renames an item.","anyOf":[{"type":"number"},{"type":"null"}]},"renewalIntervalMonths":{"description":"How often this service comes back around, in months. Null means it does not recur: a removal happens once. The renewal due date is DERIVED from this and the date the work was done, never stored, so changing it changes what is due immediately.","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","type","name","description","category","unitPriceCents","active","workTypeHint","dbhMinIn","dbhMaxIn","renewalIntervalMonths","createdAt","updatedAt"],"additionalProperties":false,"id":"CatalogItem"},"Estimate":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"number":{"description":"\"JOB-0007\", which is what the customer sees.","type":"string"},"status":{"type":"string"},"siteId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"customerId":{"description":"Who pays. Always set on an estimate created through this API: it is taken from the site, because you already named the customer when you created the site. It can be null on an estimate created in the app, where attaching a customer is a separate step. An invoice cannot be issued without it.","anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$"},{"type":"null"}]},"origin":{"type":"string"},"totalCents":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"unpricedCount":{"description":"Non-zero means this estimate cannot be sent as-is.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"narrative":{"anyOf":[{"type":"string"},{"type":"null"}]},"flags":{"type":"array","items":{"type":"string"}},"expiresAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"sentAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"acceptedAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"lines":{"type":"array","items":{"$ref":"#/components/schemas/EstimateLine"}},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","number","status","siteId","customerId","origin","totalCents","unpricedCount","narrative","flags","expiresAt","sentAt","acceptedAt","lines","createdAt","updatedAt"],"additionalProperties":false,"id":"Estimate"},"EstimateLine":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"lineNumber":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"description":{"anyOf":[{"type":"string"},{"type":"null"}]},"coverage":{"description":"What this line covers, as it appears on the customer document.","anyOf":[{"type":"string"},{"type":"null"}]},"quantity":{"type":"number"},"unitPriceCents":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"amountCents":{"description":"Null means UNPRICED (no rate on file). It is not zero, and treating it as zero publishes free work.","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"status":{"description":"`pending` until a crew marks the work done in the app. Nothing in this API changes it.","type":"string"},"catalogItemId":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},{"type":"null"}]},"siteId":{"description":"Null on a single-site estimate, which is the normal case: the line belongs to the estimate's own site. Only set on an estimate that covers more than one site.","anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$"},{"type":"null"}]},"treeIds":{"description":"Which captured trees this line prices. Empty when the line was quoted by count rather than from inventory, which is what every line created through this API is.","type":"array","items":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"}}},"required":["id","lineNumber","description","coverage","quantity","unitPriceCents","amountCents","status","catalogItemId","siteId","treeIds"],"additionalProperties":false,"id":"EstimateLine"},"Invoice":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"number":{"description":"\"INV-0051\" once posted. Null while the invoice is still a draft.","anyOf":[{"type":"string"},{"type":"null"}]},"state":{"description":"draft | posted | voided.","type":"string"},"financialStatus":{"description":"Where the money stands. `open` is issued and unpaid, `partial` is part paid, `paid` is settled, `overdue` is unpaid past its due date, `voided` was cancelled. Branch on this rather than on `balanceCents`, and note that a part paid invoice reads `partial` even once it is late: payment state wins over lateness. Was documented as free text naming values this API never returns.","type":"string","enum":["draft","open","partial","paid","overdue","voided"]},"customerId":{"description":"Who owes the money.","type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"estimateId":{"description":"The estimate this was billed from, when it came from one.","anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$"},{"type":"null"}]},"estimateNumber":{"description":"\"JOB-0062\".","anyOf":[{"type":"string"},{"type":"null"}]},"currencyCode":{"type":"string"},"subtotalCents":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"discountCents":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"taxCents":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"totalCents":{"description":"What was billed.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"paidCents":{"description":"What has been received against it.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"balanceCents":{"description":"What is still owed. Read this one rather than subtracting: it is a projection that already accounts for partial payments and voids.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"lineCount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"transactionDate":{"description":"The invoice date, YYYY-MM-DD.","type":"string"},"dueDate":{"description":"YYYY-MM-DD.","type":"string"},"postedAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"voidedAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"voidedReason":{"anyOf":[{"type":"string"},{"type":"null"}]},"firstSentAt":{"description":"When a human first sent it to the customer. Not settable here.","anyOf":[{"type":"string"},{"type":"null"}]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","number","state","financialStatus","customerId","estimateId","estimateNumber","currencyCode","subtotalCents","discountCents","taxCents","totalCents","paidCents","balanceCents","lineCount","transactionDate","dueDate","postedAt","voidedAt","voidedReason","firstSentAt","createdAt","updatedAt"],"additionalProperties":false,"id":"Invoice"},"Crew":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"name":{"type":"string"},"color":{"description":"Hex, `#rrggbb`. The color this crew is drawn in on the board.","type":"string"},"active":{"description":"`false` means archived. Archived crews are returned because history keeps their identity, but do not schedule new work onto one.","type":"boolean"},"dayStartMinutes":{"description":"When this crew's working day starts, minutes past midnight. `null` means the default, 7:00am (420).","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"dayEndMinutes":{"description":"When it ends. `null` means the default, 3:00pm (900). This is the denominator of every capacity figure drawn for this crew: work planned past it is WARNED about, never blocked.","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"memberCount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"createdAt":{"description":"ISO-8601, UTC.","type":"string"},"updatedAt":{"description":"ISO-8601, UTC.","type":"string"}},"required":["id","name","color","active","dayStartMinutes","dayEndMinutes","memberCount","createdAt","updatedAt"],"additionalProperties":false,"id":"Crew"},"ScheduledWork":{"type":"object","properties":{"scheduleId":{"description":"The placement, not the estimate.","type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"estimateId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"estimateNumber":{"description":"Formatted, e.g. \"JOB-0062\".","type":"string"},"estimateStatus":{"type":"string"},"scheduledDate":{"description":"`YYYY-MM-DD`, the crew's local day.","type":"string"},"serviceCategories":{"description":"Distinct catalogue categories across this estimate's lines. Also a query parameter on this endpoint.","type":"array","items":{"type":"string"}},"districtIds":{"description":"Every named area this day's SITE falls in. Also a query parameter, and returned so a `districtIds` filter can be checked against the rows it gives back rather than taken on trust.","type":"array","items":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"}},"sortOrder":{"description":"Position within that crew's day, ascending. This is the drive order.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"note":{"anyOf":[{"type":"string"},{"type":"null"}]},"siteId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"siteLabel":{"type":"string"},"latitude":{"anyOf":[{"type":"number"},{"type":"null"}]},"longitude":{"anyOf":[{"type":"number"},{"type":"null"}]},"totalCents":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"plannedHours":{"description":"How many of the estimate's hours are planned onto THIS day. A job spread over several days carries a share on each, and they need not be equal: a twelve-hour removal and a two-hour stump grind are one estimate across two placements. `null` when nobody has estimated the work.","anyOf":[{"type":"number"},{"type":"null"}]},"crew":{"description":"`null` means placed on the day with no crew yet (\"pencilled\").","anyOf":[{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"name":{"type":"string"},"color":{"type":"string"},"isArchived":{"type":"boolean"}},"required":["id","name","color","isArchived"],"additionalProperties":false},{"type":"null"}]}},"required":["scheduleId","estimateId","estimateNumber","estimateStatus","scheduledDate","serviceCategories","districtIds","sortOrder","note","siteId","siteLabel","latitude","longitude","totalCents","plannedHours","crew"],"additionalProperties":false,"id":"ScheduledWork"}},"responses":{"Unauthorized":{"description":"Missing, malformed, revoked or expired credential. The `WWW-Authenticate` header names the scheme, which is `ApiKey`. Carries no `RateLimit-*` headers: the credential was never resolved, so there is no bucket to report.","headers":{"WWW-Authenticate":{"$ref":"#/components/headers/WWW-Authenticate"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"message":"Invalid API key.","data":{"code":"unauthorized"}}}}},"Forbidden":{"description":"The 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 no `RateLimit-*` headers: a request refused for scope is not counted against your limit.","headers":{},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"message":"This key does not carry the `estimates:write` scope. It has: inventory:read, customers:read.","data":{"code":"forbidden"}}}}},"NotFound":{"description":"No such record in your organization. A record belonging to somebody else is indistinguishable from one that does not exist.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"message":"Estimate not found","data":{"code":"not_found"}}}}},"ValidationFailed":{"description":"Validation failed. Every bad field is named, at once.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"},"example":{"success":false,"message":"Validation failed","data":{"errors":[{"field":"siteId","problem":"siteId must be a UUID"},{"field":"lines","problem":"an estimate needs at least one line"}]}}}}},"IdempotencyConflict":{"description":"Idempotency conflict. `idempotency_key_reused` means this `Idempotency-Key` was already used with a DIFFERENT request body: mint a new key for a new request, or resend the original body to replay. `command_in_progress` means an identical request is still in flight; wait `Retry-After` seconds and retry.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"},"Retry-After":{"$ref":"#/components/headers/Retry-After"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"message":"This Idempotency-Key was already used with a different request body. Use a new key for a new request, or resend the original body to replay.","data":{"code":"idempotency_key_reused"}}}}},"RateLimited":{"description":"Rate limit exceeded.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"},"Retry-After":{"$ref":"#/components/headers/Retry-After"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"message":"Rate limit of 1000 requests per hour exceeded for this key.","data":{"code":"rate_limited"}}}}},"ServerError":{"description":"Something failed on our side. Retry with the SAME Idempotency-Key; a 5xx is the one status where retrying is correct.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"message":"Something went wrong on our side. The request was not completed.","data":{"code":"internal_error"}}}}}},"securitySchemes":{"ApiKey":{"type":"apiKey","in":"header","name":"Authorization","description":"The literal header is `Authorization: ApiKey tiai_...`. The scheme word is \"ApiKey\", not \"Bearer\"."}}},"paths":{"/customers":{"get":{"operationId":"listCustomers","summary":"List customers","description":"Oldest first, by creation. Use `search` to find one by name. That is the usual first call when you are working from a name on a spreadsheet.\n\nRequires the `customers:read` scope.","tags":["Customers"],"parameters":[{"name":"cursor","in":"query","required":false,"description":"Opaque. Take it from a previous response's `nextCursor` and send it back unchanged. Do not parse it; its contents are not part of this contract.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Page size. Defaults to 50, clamped to 200 rather than refused.","schema":{"type":"integer","minimum":1,"maximum":9007199254740991}},{"name":"search","in":"query","required":false,"description":"Case-insensitive substring of the customer name.","schema":{"type":"string","minLength":1,"maxLength":200}}],"responses":{"200":{"description":"List customers","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"}},"content":{"application/json":{"schema":{"description":"A page of customers.","type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Customer"}},"nextCursor":{"description":"Pass as `?cursor=` for the next page. `null` means this was the last page. Ordered oldest-first by creation, so a record created while you page appears at the end and nothing you have already seen moves.","anyOf":[{"type":"string"},{"type":"null"}]}},"required":["items","nextCursor"],"additionalProperties":false}},"required":["success","message","data"],"additionalProperties":false},"example":{"success":true,"message":"Customers retrieved","data":{"items":[{"id":"9f1b2c3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d","name":"Delap Road Properties","email":"office@delaproad.example","phone":"(812) 555-0134","createdAt":"2026-08-19T14:02:11.412Z","updatedAt":"2026-08-19T14:02:11.412Z"}],"nextCursor":"eyJ2IjoidjEiLCJjcmVhdGVkQXQiOiIyMDI2LTA4LTE5IDE0OjAyOjExLjQxMjMzMSswMCIsImlkIjoiOWYxYjJjM2QtNGU1Zi00YTZiLThjN2QtMGUxZjJhM2I0YzVkIn0"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"operationId":"createCustomer","summary":"Create a customer","description":"Send an `Idempotency-Key`. Without one, a retry after a timeout creates a second customer and somebody has to merge them by hand.\n\nRequires the `customers:write` scope.","tags":["Customers"],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"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.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Create a customer (replay of an earlier request with the same `Idempotency-Key`; nothing was created)","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"},"Idempotent-Replay":{"description":"Present and `true` only when this response is a replay of an earlier request that carried the same `Idempotency-Key`. Nothing was created or changed to produce it.","schema":{"type":"string","enum":["true"]}}},"content":{"application/json":{"schema":{"description":"One customer.","type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"$ref":"#/components/schemas/Customer"}},"required":["success","message","data"],"additionalProperties":false},"example":{"success":true,"message":"Customer created","data":{"id":"9f1b2c3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d","name":"Delap Road Properties","email":"office@delaproad.example","phone":"(812) 555-0134","createdAt":"2026-08-19T14:02:11.412Z","updatedAt":"2026-08-19T14:02:11.412Z"}}}}},"201":{"description":"Create a customer","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"},"Idempotent-Replay":{"description":"Present and `true` only when this response is a replay of an earlier request that carried the same `Idempotency-Key`. Nothing was created or changed to produce it.","schema":{"type":"string","enum":["true"]}}},"content":{"application/json":{"schema":{"description":"One customer.","type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"$ref":"#/components/schemas/Customer"}},"required":["success","message","data"],"additionalProperties":false},"example":{"success":true,"message":"Customer created","data":{"id":"9f1b2c3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d","name":"Delap Road Properties","email":"office@delaproad.example","phone":"(812) 555-0134","createdAt":"2026-08-19T14:02:11.412Z","updatedAt":"2026-08-19T14:02:11.412Z"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"email":{"anyOf":[{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}]},"phone":{"anyOf":[{"type":"string","maxLength":40},{"type":"null"}]}},"required":["name"]},"example":{"name":"Delap Road Properties","email":"office@delaproad.example","phone":"(812) 555-0134"}}}}}},"/sites":{"get":{"operationId":"listSites","summary":"List sites","description":"Filter by `customerId` to get one customer's properties. A customerId belonging to another organization returns an empty page, not an error, because confirming that an id exists elsewhere is itself a leak.\n\nRequires the `sites:read` scope.","tags":["Sites"],"parameters":[{"name":"cursor","in":"query","required":false,"description":"Opaque. Take it from a previous response's `nextCursor` and send it back unchanged. Do not parse it; its contents are not part of this contract.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Page size. Defaults to 50, clamped to 200 rather than refused.","schema":{"type":"integer","minimum":1,"maximum":9007199254740991}},{"name":"customerId","in":"query","required":false,"description":"Only this customer's sites.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$"}},{"name":"search","in":"query","required":false,"description":"Case-insensitive substring of the street line.","schema":{"type":"string","minLength":1,"maxLength":200}}],"responses":{"200":{"description":"List sites","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"}},"content":{"application/json":{"schema":{"description":"A page of sites.","type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Site"}},"nextCursor":{"description":"Pass as `?cursor=` for the next page. `null` means this was the last page. Ordered oldest-first by creation, so a record created while you page appears at the end and nothing you have already seen moves.","anyOf":[{"type":"string"},{"type":"null"}]}},"required":["items","nextCursor"],"additionalProperties":false}},"required":["success","message","data"],"additionalProperties":false},"example":{"success":true,"message":"Sites retrieved","data":{"items":[{"id":"3a7c9e21-5b48-4f0a-9d33-71c6ee204b18","customerId":"9f1b2c3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d","label":"Front acre","street":"3760 W Delap Rd","city":"Bloomington","state":"IN","zip":"47404","country":"US","latitude":39.1912,"longitude":-86.5847,"createdAt":"2026-08-19T14:05:47.008Z","updatedAt":"2026-08-19T14:05:47.008Z"}],"nextCursor":null}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"operationId":"createSite","summary":"Create a site","description":"Omit `latitude`/`longitude` and the address is geocoded for you. That is usually what you want.\n\nRequires the `sites:write` scope.","tags":["Sites"],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"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.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Create a site (replay of an earlier request with the same `Idempotency-Key`; nothing was created)","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"},"Idempotent-Replay":{"description":"Present and `true` only when this response is a replay of an earlier request that carried the same `Idempotency-Key`. Nothing was created or changed to produce it.","schema":{"type":"string","enum":["true"]}}},"content":{"application/json":{"schema":{"description":"One site.","type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"$ref":"#/components/schemas/Site"}},"required":["success","message","data"],"additionalProperties":false},"example":{"success":true,"message":"Site created","data":{"id":"3a7c9e21-5b48-4f0a-9d33-71c6ee204b18","customerId":"9f1b2c3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d","label":"Front acre","street":"3760 W Delap Rd","city":"Bloomington","state":"IN","zip":"47404","country":"US","latitude":39.1912,"longitude":-86.5847,"createdAt":"2026-08-19T14:05:47.008Z","updatedAt":"2026-08-19T14:05:47.008Z"}}}}},"201":{"description":"Create a site","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"},"Idempotent-Replay":{"description":"Present and `true` only when this response is a replay of an earlier request that carried the same `Idempotency-Key`. Nothing was created or changed to produce it.","schema":{"type":"string","enum":["true"]}}},"content":{"application/json":{"schema":{"description":"One site.","type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"$ref":"#/components/schemas/Site"}},"required":["success","message","data"],"additionalProperties":false},"example":{"success":true,"message":"Site created","data":{"id":"3a7c9e21-5b48-4f0a-9d33-71c6ee204b18","customerId":"9f1b2c3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d","label":"Front acre","street":"3760 W Delap Rd","city":"Bloomington","state":"IN","zip":"47404","country":"US","latitude":39.1912,"longitude":-86.5847,"createdAt":"2026-08-19T14:05:47.008Z","updatedAt":"2026-08-19T14:05:47.008Z"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"customerId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$"},"label":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}]},"street":{"type":"string","minLength":1,"maxLength":300},"city":{"type":"string","minLength":1,"maxLength":200},"state":{"type":"string","minLength":1,"maxLength":100},"zip":{"type":"string","minLength":1,"maxLength":20},"country":{"default":"US","type":"string","minLength":1,"maxLength":100},"latitude":{"description":"Optional. Omit it and we geocode the address. Send it and we keep it only if it agrees with the geocode, because a phone's GPS at the moment a form opened is not evidence about the address typed into it.","anyOf":[{"type":"number","minimum":-90,"maximum":90},{"type":"null"}]},"longitude":{"anyOf":[{"type":"number","minimum":-180,"maximum":180},{"type":"null"}]}},"required":["customerId","street","city","state","zip"]},"example":{"customerId":"9f1b2c3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d","label":"Front acre","street":"3760 W Delap Rd","city":"Bloomington","state":"IN","zip":"47404","country":"US"}}}}}},"/sites/{siteId}/trees":{"get":{"operationId":"listTreesAtSite","summary":"List the trees captured at a site","description":"Not paginated. A site is one property walked by one arborist, so this is tens of records; a cursor here would cost you a loop and gain you nothing.\n\nRequires the `inventory:read` scope.","tags":["Trees"],"parameters":[{"name":"siteId","in":"path","required":true,"description":"The site's id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"List the trees captured at a site","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"}},"content":{"application/json":{"schema":{"description":"Every tree at one site. Not paginated: a site is one property walked by one arborist, so this is tens of records, not thousands.","type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Tree"}}},"required":["items"],"additionalProperties":false}},"required":["success","message","data"],"additionalProperties":false},"example":{"success":true,"message":"Trees retrieved","data":{"items":[{"id":"c04f8a55-1d2e-4b39-8a71-6f5c2d9e3b40","siteId":"3a7c9e21-5b48-4f0a-9d33-71c6ee204b18","commonName":"Northern Red Oak","scientificName":"Quercus rubra","speciesConfidence":0.91,"speciesSource":"user-confirmed","dbhInches":22,"dbhConfidence":0.44,"heightFeet":58,"heightConfidence":0.6,"canopySpreadFeet":34,"canopyConfidence":0.55,"trunkCount":1,"quantity":1,"healthCondition":"fair","hazardRating":3,"riskAssessment":"Deadwood over the driveway; included bark at the main union.","defects":["included bark"],"observations":["Deadwood concentrated on the south side"],"recommendations":["Crown clean","Reduce the driveway-side limb"],"tags":[],"latitude":39.19124,"longitude":-86.58471,"gpsAccuracyMeters":4,"capturedAt":"2026-08-20T15:41:02.000Z","createdAt":"2026-08-20T15:41:05.220Z","updatedAt":"2026-08-20T15:41:05.220Z"}]}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/trees/{treeId}":{"get":{"operationId":"getTree","summary":"Get one tree","description":"A tree in another organization answers 404, identically to one that does not exist.\n\nRequires the `inventory:read` scope.","tags":["Trees"],"parameters":[{"name":"treeId","in":"path","required":true,"description":"The tree's id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Get one tree","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"}},"content":{"application/json":{"schema":{"description":"One captured tree.","type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"$ref":"#/components/schemas/Tree"}},"required":["success","message","data"],"additionalProperties":false},"example":{"success":true,"message":"Tree retrieved","data":{"id":"c04f8a55-1d2e-4b39-8a71-6f5c2d9e3b40","siteId":"3a7c9e21-5b48-4f0a-9d33-71c6ee204b18","commonName":"Northern Red Oak","scientificName":"Quercus rubra","speciesConfidence":0.91,"speciesSource":"user-confirmed","dbhInches":22,"dbhConfidence":0.44,"heightFeet":58,"heightConfidence":0.6,"canopySpreadFeet":34,"canopyConfidence":0.55,"trunkCount":1,"quantity":1,"healthCondition":"fair","hazardRating":3,"riskAssessment":"Deadwood over the driveway; included bark at the main union.","defects":["included bark"],"observations":["Deadwood concentrated on the south side"],"recommendations":["Crown clean","Reduce the driveway-side limb"],"tags":[],"latitude":39.19124,"longitude":-86.58471,"gpsAccuracyMeters":4,"capturedAt":"2026-08-20T15:41:02.000Z","createdAt":"2026-08-20T15:41:05.220Z","updatedAt":"2026-08-20T15:41:05.220Z"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/trees/{treeId}/photos":{"get":{"operationId":"listTreePhotos","summary":"Look at the photographs of one tree","description":"The photographs are the arborist's actual account of the visit, and everything else published about a tree is a text summary a vision model wrote from ONE of them. The `defect` close-up somebody stopped to take a second photo of has never been read by anything. If you can see images, look before you price: a 24in maple with `included bark` in `defects` is a different job depending on whether the union is cabled, and the photograph settles it and the summary does not.\n\nNot paginated. `url` is a SHORT-LIVED SIGNED URL: fetch it now, do not store it, do not put it in a document, and do not hand it to a customer. Ask again when it expires.\n\nRequires the `inventory:read` scope.","tags":["Trees"],"parameters":[{"name":"treeId","in":"path","required":true,"description":"The tree's id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Look at the photographs of one tree","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"}},"content":{"application/json":{"schema":{"description":"Every photograph of one tree. Not paginated: this is a handful.","type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"treeId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"category":{"description":"What the arborist was photographing: `primary`, `trunk`, `canopy`, `defect`, `context`, `markup` or `general`. `primary` is the one the species and measurements were inferred from. `defect` is the one worth looking at hardest, because it is the shot somebody chose to take a second photo for.","type":"string"},"caption":{"anyOf":[{"type":"string"},{"type":"null"}]},"hasMarkup":{"description":"The arborist drew on this photo: an arrow at the limb, a circle round the union. A true here means a person pointed at something.","type":"boolean"},"width":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"height":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"capturedAt":{"type":"string"},"url":{"description":"A short-lived signed URL. Fetch it now or ask again later. Null means the file could not be signed, which is a storage problem and not an empty photograph.","anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","treeId","category","caption","hasMarkup","width","height","capturedAt","url"],"additionalProperties":false}}},"required":["items"],"additionalProperties":false}},"required":["success","message","data"],"additionalProperties":false},"example":{"success":true,"message":"Photos retrieved","data":{"items":[{"id":"d1e2f3a4-b5c6-4d7e-8f90-1a2b3c4d5e6f","treeId":"c04f8a55-1d2e-4b39-8a71-6f5c2d9e3b40","category":"defect","caption":"Included bark at the main union","hasMarkup":true,"width":3024,"height":4032,"capturedAt":"2026-08-20T15:41:04.000Z","url":"https://storage.treeinventory.ai/signed/..."}]}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/sites/{siteId}/notes":{"get":{"operationId":"listSiteNotes","summary":"Read what was said on the walk","description":"Every note taken at a property, the ones dictated standing at a tree included, oldest first. THIS IS WHERE THE QUOTE IS ACTUALLY DECIDED. An arborist holds a button and says what is wrong and what it will cost, and pastes the customer conversation against the site afterwards; `estimatedCostCents` is the number they said out loud and `clientNotes` is what the customer asked for. Read this BEFORE you ask an arborist anything, because most of what you were about to ask is in here, and asking for a price they already dictated is how an assistant tells somebody it was not listening.\n\n`mentionedTrees` carries species plus position as they said it, which is how `the big oak by the driveway` becomes an id. Not paginated: one visit is a handful of notes.\n\nRequires the `inventory:read` scope.","tags":["Notes"],"parameters":[{"name":"siteId","in":"path","required":true,"description":"The site's id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Read what was said on the walk","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"}},"content":{"application/json":{"schema":{"description":"Every note taken at a site, its trees included, oldest first. Not paginated: one visit is a handful of notes.","type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"scope":{"description":"`site` when it was dictated about the property, `tree` when it was dictated standing at one.","type":"string"},"treeId":{"description":"The tree it was dictated at. Null on a site note.","anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$"},{"type":"null"}]},"transcript":{"description":"What was said, verbatim. Read this before the extraction.","type":"string"},"estimatedCostCents":{"description":"A price the arborist said out loud, in whole cents. THIS IS THE NUMBER THEY QUOTED THE CUSTOMER. Where it exists it beats the catalog, and asking for a price it already carries is the single most annoying thing an assistant can do.","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"clientNotes":{"description":"What the customer said they wanted, in the arborist's retelling. This is the brief, and it is why the customer's own words are worth more than any inference from the inventory.","anyOf":[{"type":"string"},{"type":"null"}]},"health":{"anyOf":[{"type":"string"},{"type":"null"}]},"defects":{"type":"array","items":{"type":"string"}},"observations":{"type":"array","items":{"type":"string"}},"recommendations":{"type":"array","items":{"type":"string"}},"mentionedTrees":{"description":"Trees named in the talking, as species plus where they stand. Match these against the captured records; this is how `the big oak by the driveway` becomes an id.","type":"array","items":{"type":"object","properties":{"species":{"type":"string"},"location":{"type":"string"}},"required":["species","location"],"additionalProperties":false}},"durationSeconds":{"anyOf":[{"type":"number"},{"type":"null"}]},"createdAt":{"type":"string"}},"required":["id","scope","treeId","transcript","estimatedCostCents","clientNotes","health","defects","observations","recommendations","mentionedTrees","durationSeconds","createdAt"],"additionalProperties":false}}},"required":["items"],"additionalProperties":false}},"required":["success","message","data"],"additionalProperties":false},"example":{"success":true,"message":"Notes retrieved","data":{"items":[{"id":"8c7d6e5f-4a3b-4c2d-9e1f-0a9b8c7d6e5f","scope":"tree","treeId":"c04f8a55-1d2e-4b39-8a71-6f5c2d9e3b40","transcript":"Big red oak over the driveway, deadwood through the top, included bark at the union. Told them about fifteen hundred for the crown clean and a cable. They only want the driveway side done this year.","estimatedCostCents":150000,"clientNotes":"Only wants the driveway side done this year.","health":"fair","defects":["included bark","deadwood"],"observations":["Deadwood concentrated over the parking pad"],"recommendations":["crown clean","cable/brace"],"mentionedTrees":[{"species":"red oak","location":"over the driveway"}],"durationSeconds":22,"createdAt":"2026-08-20T15:42:10.000Z"}]}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/catalog/items":{"get":{"operationId":"listCatalogItems","summary":"List catalog items","description":"Read this before writing an estimate. Every estimate line references a `catalogItemId` from here, and an id you invented is a 404 you cannot diagnose from the outside.\n\nRequires the `catalog:read` scope.","tags":["Catalog"],"parameters":[{"name":"cursor","in":"query","required":false,"description":"Opaque. Take it from a previous response's `nextCursor` and send it back unchanged. Do not parse it; its contents are not part of this contract.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Page size. Defaults to 50, clamped to 200 rather than refused.","schema":{"type":"integer","minimum":1,"maximum":9007199254740991}},{"name":"activeOnly","in":"query","required":false,"description":"Defaults to true. Retired items are rarely the useful answer.","schema":{"type":"string","enum":["true","false"]}}],"responses":{"200":{"description":"List catalog items","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"}},"content":{"application/json":{"schema":{"description":"A page of catalog items.","type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/CatalogItem"}},"nextCursor":{"description":"Pass as `?cursor=` for the next page. `null` means this was the last page. Ordered oldest-first by creation, so a record created while you page appears at the end and nothing you have already seen moves.","anyOf":[{"type":"string"},{"type":"null"}]}},"required":["items","nextCursor"],"additionalProperties":false}},"required":["success","message","data"],"additionalProperties":false},"example":{"success":true,"message":"Catalog items retrieved","data":{"items":[{"id":"7b1d0c94-2f63-4e8a-9a55-c3d8e0f1a2b3","type":"service","name":"Crown clean","description":"Remove dead, dying, diseased and crossing branches.","category":"pruning","unitPriceCents":45000,"active":true,"workTypeHint":"trim","dbhMinIn":12,"dbhMaxIn":24,"renewalIntervalMonths":null,"createdAt":"2026-06-02T09:12:00.000Z","updatedAt":"2026-06-02T09:12:00.000Z"}],"nextCursor":null}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/estimates":{"post":{"operationId":"createEstimate","summary":"Create an estimate","description":"Post the whole quote at once. Totals are computed here from the quantities and unit prices you send; do not send a total, it would be ignored. Send an `Idempotency-Key`: a timed-out retry without one creates a second quote, and the arborist finds out when a customer asks which is real.\n\nRequires the `estimates:write` scope.","tags":["Estimates"],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"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.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Create an estimate (replay of an earlier request with the same `Idempotency-Key`; nothing was created)","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"},"Idempotent-Replay":{"description":"Present and `true` only when this response is a replay of an earlier request that carried the same `Idempotency-Key`. Nothing was created or changed to produce it.","schema":{"type":"string","enum":["true"]}}},"content":{"application/json":{"schema":{"description":"One estimate.","type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"$ref":"#/components/schemas/Estimate"}},"required":["success","message","data"],"additionalProperties":false},"example":{"success":true,"message":"Estimate created","data":{"id":"e5d4c3b2-a190-4877-b6e5-d4c3b2a19087","number":"JOB-0062","status":"draft","siteId":"3a7c9e21-5b48-4f0a-9d33-71c6ee204b18","customerId":"9f1b2c3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d","origin":"manual","totalCents":122500,"unpricedCount":0,"narrative":null,"flags":[],"expiresAt":"2026-09-25T00:00:00.000Z","sentAt":null,"acceptedAt":null,"lines":[{"id":"11111111-2222-4333-8444-555555555555","lineNumber":1,"description":"Crown clean, front oaks","coverage":null,"quantity":2,"unitPriceCents":45000,"amountCents":90000,"status":"pending","catalogItemId":"7b1d0c94-2f63-4e8a-9a55-c3d8e0f1a2b3","siteId":null,"treeIds":[]},{"id":"66666666-7777-4888-8999-aaaaaaaaaaaa","lineNumber":2,"description":"Deadwood, rear maple","coverage":null,"quantity":1,"unitPriceCents":32500,"amountCents":32500,"status":"pending","catalogItemId":"7b1d0c94-2f63-4e8a-9a55-c3d8e0f1a2b3","siteId":null,"treeIds":[]}],"createdAt":"2026-08-25T18:22:41.771Z","updatedAt":"2026-08-25T18:22:41.771Z"}}}}},"201":{"description":"Create an estimate","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"},"Idempotent-Replay":{"description":"Present and `true` only when this response is a replay of an earlier request that carried the same `Idempotency-Key`. Nothing was created or changed to produce it.","schema":{"type":"string","enum":["true"]}}},"content":{"application/json":{"schema":{"description":"One estimate.","type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"$ref":"#/components/schemas/Estimate"}},"required":["success","message","data"],"additionalProperties":false},"example":{"success":true,"message":"Estimate created","data":{"id":"e5d4c3b2-a190-4877-b6e5-d4c3b2a19087","number":"JOB-0062","status":"draft","siteId":"3a7c9e21-5b48-4f0a-9d33-71c6ee204b18","customerId":"9f1b2c3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d","origin":"manual","totalCents":122500,"unpricedCount":0,"narrative":null,"flags":[],"expiresAt":"2026-09-25T00:00:00.000Z","sentAt":null,"acceptedAt":null,"lines":[{"id":"11111111-2222-4333-8444-555555555555","lineNumber":1,"description":"Crown clean, front oaks","coverage":null,"quantity":2,"unitPriceCents":45000,"amountCents":90000,"status":"pending","catalogItemId":"7b1d0c94-2f63-4e8a-9a55-c3d8e0f1a2b3","siteId":null,"treeIds":[]},{"id":"66666666-7777-4888-8999-aaaaaaaaaaaa","lineNumber":2,"description":"Deadwood, rear maple","coverage":null,"quantity":1,"unitPriceCents":32500,"amountCents":32500,"status":"pending","catalogItemId":"7b1d0c94-2f63-4e8a-9a55-c3d8e0f1a2b3","siteId":null,"treeIds":[]}],"createdAt":"2026-08-25T18:22:41.771Z","updatedAt":"2026-08-25T18:22:41.771Z"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"siteId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$"},"lines":{"minItems":1,"maxItems":200,"type":"array","items":{"type":"object","properties":{"catalogItemId":{"description":"From GET /catalog/items. Inventing one is a 404. If nothing in the catalog matches the work you are quoting, pick the NEAREST item and override `description` and `unitPriceCents`. That is the intended pattern, not a workaround: the catalog carries the org's default prices, and your line carries what you are actually charging.","type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$"},"description":{"description":"What the customer reads on this line. Send the real work here when the catalog item you referenced is only the nearest match.","type":"string","minLength":1,"maxLength":500},"quantity":{"description":"A whole number, in the catalog item's own unit. Most are per tree; a few name their unit in the item name, such as `Lawn Mowing (per 100 sq ft)`, and there you send the number of units, so 250 sq ft of mowing is a quantity of 3. Fractions are not stored: the column is an integer, and sending 2.5 used to fail as a 500 rather than tell you this.","type":"integer","exclusiveMinimum":0,"maximum":1000000},"unitPriceCents":{"description":"Whole US cents. 45000 is $450.00. There is no currency field.","type":"integer","minimum":0,"maximum":9007199254740991}},"required":["catalogItemId","quantity","unitPriceCents"]}}},"required":["siteId","lines"]},"example":{"siteId":"3a7c9e21-5b48-4f0a-9d33-71c6ee204b18","lines":[{"catalogItemId":"7b1d0c94-2f63-4e8a-9a55-c3d8e0f1a2b3","description":"Crown clean, front oaks","quantity":2,"unitPriceCents":45000},{"catalogItemId":"7b1d0c94-2f63-4e8a-9a55-c3d8e0f1a2b3","description":"Deadwood, rear maple","quantity":1,"unitPriceCents":32500}]}}}}}},"/estimates/{estimateId}":{"get":{"operationId":"getEstimate","summary":"Get one estimate","description":"The read that makes a write checkable.\n\nRequires the `estimates:read` scope.","tags":["Estimates"],"parameters":[{"name":"estimateId","in":"path","required":true,"description":"The estimate's id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Get one estimate","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"}},"content":{"application/json":{"schema":{"description":"One estimate.","type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"$ref":"#/components/schemas/Estimate"}},"required":["success","message","data"],"additionalProperties":false},"example":{"success":true,"message":"Estimate retrieved","data":{"id":"e5d4c3b2-a190-4877-b6e5-d4c3b2a19087","number":"JOB-0062","status":"draft","siteId":"3a7c9e21-5b48-4f0a-9d33-71c6ee204b18","customerId":"9f1b2c3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d","origin":"manual","totalCents":122500,"unpricedCount":0,"narrative":null,"flags":[],"expiresAt":"2026-09-25T00:00:00.000Z","sentAt":null,"acceptedAt":null,"lines":[{"id":"11111111-2222-4333-8444-555555555555","lineNumber":1,"description":"Crown clean, front oaks","coverage":null,"quantity":2,"unitPriceCents":45000,"amountCents":90000,"status":"pending","catalogItemId":"7b1d0c94-2f63-4e8a-9a55-c3d8e0f1a2b3","siteId":null,"treeIds":[]},{"id":"66666666-7777-4888-8999-aaaaaaaaaaaa","lineNumber":2,"description":"Deadwood, rear maple","coverage":null,"quantity":1,"unitPriceCents":32500,"amountCents":32500,"status":"pending","catalogItemId":"7b1d0c94-2f63-4e8a-9a55-c3d8e0f1a2b3","siteId":null,"treeIds":[]}],"createdAt":"2026-08-25T18:22:41.771Z","updatedAt":"2026-08-25T18:22:41.771Z"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/estimates/{estimateId}/accept":{"post":{"operationId":"acceptEstimate","summary":"Accept an estimate","description":"Record that the customer said yes. This is the step that turns a quote into work: an accepted estimate is what scheduling and routing look at, and a draft is invisible to both. Nothing is emailed to the customer from here. A draft or a sent estimate can be accepted; accepting one that is already accepted replays and answers 200, so a retry after a timeout is safe without an `Idempotency-Key`. A declined or canceled estimate answers 422.\n\nRequires the `estimates:write` scope.","tags":["Estimates"],"parameters":[{"name":"estimateId","in":"path","required":true,"description":"The estimate's id.","schema":{"type":"string","format":"uuid"}},{"name":"Idempotency-Key","in":"header","required":false,"description":"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.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Accept an estimate","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"},"Idempotent-Replay":{"description":"Present and `true` only when this response is a replay of an earlier request that carried the same `Idempotency-Key`. Nothing was created or changed to produce it.","schema":{"type":"string","enum":["true"]}}},"content":{"application/json":{"schema":{"description":"One estimate.","type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"$ref":"#/components/schemas/Estimate"}},"required":["success","message","data"],"additionalProperties":false},"example":{"success":true,"message":"Estimate accepted","data":{"id":"e5d4c3b2-a190-4877-b6e5-d4c3b2a19087","number":"JOB-0062","status":"accepted","siteId":"3a7c9e21-5b48-4f0a-9d33-71c6ee204b18","customerId":"9f1b2c3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d","origin":"manual","totalCents":122500,"unpricedCount":0,"narrative":null,"flags":[],"expiresAt":"2026-09-25T00:00:00.000Z","sentAt":null,"acceptedAt":"2026-03-04T16:12:09.482Z","lines":[{"id":"11111111-2222-4333-8444-555555555555","lineNumber":1,"description":"Crown clean, front oaks","coverage":null,"quantity":2,"unitPriceCents":45000,"amountCents":90000,"status":"pending","catalogItemId":"7b1d0c94-2f63-4e8a-9a55-c3d8e0f1a2b3","siteId":null,"treeIds":[]},{"id":"66666666-7777-4888-8999-aaaaaaaaaaaa","lineNumber":2,"description":"Deadwood, rear maple","coverage":null,"quantity":1,"unitPriceCents":32500,"amountCents":32500,"status":"pending","catalogItemId":"7b1d0c94-2f63-4e8a-9a55-c3d8e0f1a2b3","siteId":null,"treeIds":[]}],"createdAt":"2026-08-25T18:22:41.771Z","updatedAt":"2026-08-25T18:22:41.771Z"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/invoices":{"get":{"operationId":"listInvoices","summary":"List invoices","description":"Oldest first, by creation. Filter by `customerId` to see one customer's billing, or by `state`. Read `balanceCents` for what is still owed rather than subtracting the amounts yourself.\n\nRequires the `invoices:read` scope.","tags":["Invoices"],"parameters":[{"name":"cursor","in":"query","required":false,"description":"Opaque. Take it from a previous response's `nextCursor` and send it back unchanged. Do not parse it; its contents are not part of this contract.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Page size. Defaults to 50, clamped to 200 rather than refused.","schema":{"type":"integer","minimum":1,"maximum":9007199254740991}},{"name":"customerId","in":"query","required":false,"description":"Only this customer's invoices.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$"}},{"name":"state","in":"query","required":false,"description":"Only invoices in this state.","schema":{"type":"string","enum":["draft","posted","voided"]}}],"responses":{"200":{"description":"List invoices","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"}},"content":{"application/json":{"schema":{"description":"A page of invoices.","type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Invoice"}},"nextCursor":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["items","nextCursor"],"additionalProperties":false}},"required":["success","message","data"],"additionalProperties":false},"example":{"success":true,"message":"Invoices retrieved","data":{"items":[{"id":"b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e","number":"INV-0051","state":"posted","financialStatus":"open","customerId":"9f1b2c3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d","estimateId":"e5d4c3b2-a190-4877-b6e5-d4c3b2a19087","estimateNumber":"JOB-0062","currencyCode":"USD","subtotalCents":122500,"discountCents":0,"taxCents":0,"totalCents":122500,"paidCents":0,"balanceCents":122500,"lineCount":2,"transactionDate":"2026-08-30","dueDate":"2026-09-29","postedAt":"2026-08-30T15:04:22.118Z","voidedAt":null,"voidedReason":null,"firstSentAt":null,"createdAt":"2026-08-30T15:04:22.118Z","updatedAt":"2026-08-30T15:04:22.118Z"}],"nextCursor":null}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"operationId":"createInvoice","summary":"Bill a completed estimate","description":"Turns one COMPLETED estimate into an invoice. This is the call that bills a customer, so send an `Idempotency-Key`: a timed-out retry without one can raise a second invoice against the same work.\n\nThe estimate must be complete, fully priced and total more than zero. An estimate created through this API starts as a draft, and only a person can mark the work done, so in practice you are billing something somebody finished in the app. A 422 tells you which of those conditions failed.\n\nRequires the `invoices:write` scope.","tags":["Invoices"],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"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.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Bill a completed estimate (replay of an earlier request with the same `Idempotency-Key`; nothing was created)","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"},"Idempotent-Replay":{"description":"Present and `true` only when this response is a replay of an earlier request that carried the same `Idempotency-Key`. Nothing was created or changed to produce it.","schema":{"type":"string","enum":["true"]}}},"content":{"application/json":{"schema":{"description":"One invoice.","type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"$ref":"#/components/schemas/Invoice"}},"required":["success","message","data"],"additionalProperties":false},"example":{"success":true,"message":"Invoice issued","data":{"id":"b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e","number":"INV-0051","state":"posted","financialStatus":"open","customerId":"9f1b2c3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d","estimateId":"e5d4c3b2-a190-4877-b6e5-d4c3b2a19087","estimateNumber":"JOB-0062","currencyCode":"USD","subtotalCents":122500,"discountCents":0,"taxCents":0,"totalCents":122500,"paidCents":0,"balanceCents":122500,"lineCount":2,"transactionDate":"2026-08-30","dueDate":"2026-09-29","postedAt":"2026-08-30T15:04:22.118Z","voidedAt":null,"voidedReason":null,"firstSentAt":null,"createdAt":"2026-08-30T15:04:22.118Z","updatedAt":"2026-08-30T15:04:22.118Z"}}}}},"201":{"description":"Bill a completed estimate","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"},"Idempotent-Replay":{"description":"Present and `true` only when this response is a replay of an earlier request that carried the same `Idempotency-Key`. Nothing was created or changed to produce it.","schema":{"type":"string","enum":["true"]}}},"content":{"application/json":{"schema":{"description":"One invoice.","type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"$ref":"#/components/schemas/Invoice"}},"required":["success","message","data"],"additionalProperties":false},"example":{"success":true,"message":"Invoice issued","data":{"id":"b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e","number":"INV-0051","state":"posted","financialStatus":"open","customerId":"9f1b2c3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d","estimateId":"e5d4c3b2-a190-4877-b6e5-d4c3b2a19087","estimateNumber":"JOB-0062","currencyCode":"USD","subtotalCents":122500,"discountCents":0,"taxCents":0,"totalCents":122500,"paidCents":0,"balanceCents":122500,"lineCount":2,"transactionDate":"2026-08-30","dueDate":"2026-09-29","postedAt":"2026-08-30T15:04:22.118Z","voidedAt":null,"voidedReason":null,"firstSentAt":null,"createdAt":"2026-08-30T15:04:22.118Z","updatedAt":"2026-08-30T15:04:22.118Z"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"estimateId":{"description":"The estimate to bill. It must be COMPLETED, fully priced, and have a total above zero. An estimate created through this API starts as a draft and is completed by a person in the app, so this is normally a call you make about work somebody has already finished.","type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$"},"transactionDate":{"description":"The invoice date. Defaults to today.","type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"dueDate":{"description":"Defaults to 30 days after the transaction date. Cannot be earlier than it.","type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},"required":["estimateId"]},"example":{"estimateId":"e5d4c3b2-a190-4877-b6e5-d4c3b2a19087","dueDate":"2026-09-29"}}}}}},"/invoices/{invoiceId}":{"get":{"operationId":"getInvoice","summary":"Get one invoice","description":"Whether it is paid, and what is left. `paidCents` and `balanceCents` come from a projection that already accounts for partial payments and voids.\n\nRequires the `invoices:read` scope.","tags":["Invoices"],"parameters":[{"name":"invoiceId","in":"path","required":true,"description":"The invoice's id.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Get one invoice","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"}},"content":{"application/json":{"schema":{"description":"One invoice.","type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"$ref":"#/components/schemas/Invoice"}},"required":["success","message","data"],"additionalProperties":false},"example":{"success":true,"message":"Invoice retrieved","data":{"id":"b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e","number":"INV-0051","state":"posted","financialStatus":"open","customerId":"9f1b2c3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d","estimateId":"e5d4c3b2-a190-4877-b6e5-d4c3b2a19087","estimateNumber":"JOB-0062","currencyCode":"USD","subtotalCents":122500,"discountCents":0,"taxCents":0,"totalCents":122500,"paidCents":0,"balanceCents":122500,"lineCount":2,"transactionDate":"2026-08-30","dueDate":"2026-09-29","postedAt":"2026-08-30T15:04:22.118Z","voidedAt":null,"voidedReason":null,"firstSentAt":null,"createdAt":"2026-08-30T15:04:22.118Z","updatedAt":"2026-08-30T15:04:22.118Z"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/crews":{"get":{"operationId":"listCrews","summary":"List crews","description":"Read this before scheduling. Placing work takes a `crewId` from here, and an id you invented fails the whole batch. Archived crews are included and marked `active: false`; do not place new work on one.\n\nRequires the `crews:read` scope.","tags":["Crews"],"parameters":[],"responses":{"200":{"description":"List crews","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"}},"content":{"application/json":{"schema":{"description":"Every crew in the org. Not paginated: an org has crews, not thousands of them. Read this to get the `crewId` that scheduling takes.","type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Crew"}}},"required":["items"],"additionalProperties":false}},"required":["success","message","data"],"additionalProperties":false},"example":{"success":true,"message":"Crews retrieved","data":{"items":[{"id":"d2f4a601-88b3-4c17-9e5a-3b7f0c1d2e94","name":"Crew A","color":"#2f855a","active":true,"dayStartMinutes":390,"dayEndMinutes":900,"memberCount":3,"createdAt":"2026-04-11T13:20:44.101Z","updatedAt":"2026-07-02T08:15:03.660Z"}]}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/schedule":{"get":{"operationId":"listScheduledWork","summary":"List placed work","description":"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.\n\nRequires the `schedule:read` scope.","tags":["Schedule"],"parameters":[{"name":"from","in":"query","required":false,"description":"Placements on or after this day. Defaults to today, UTC.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},{"name":"to","in":"query","required":false,"description":"Placements on or before this day, inclusive. Omit for everything from `from` onward. `needsAttention` ignores this: work that fell off the board is outstanding whatever window you asked about.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},{"name":"crewId","in":"query","required":false,"schema":{"anyOf":[{"description":"Only this crew's day. Pass the literal string `none` instead of an id for work pencilled onto a day with no crew yet, which is a board a dispatcher reads on its own.","type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$"},{"type":"string","const":"none"}]}},{"name":"q","in":"query","required":false,"description":"Free text over the site label and the job number. NOT the customer: unplaced work carries no customer name, and promising one here would be a search that silently never matches.","schema":{"type":"string","maxLength":200}},{"name":"kinds","in":"query","required":false,"description":"Which kinds to include, comma-separated. Omit for all three. `work_to_place` is accepted work with no day, `work_day` is a placed crew-day, `appointment` is a visit.","schema":{"type":"array","items":{"type":"string","enum":["work_to_place","work_day","appointment"]}}},{"name":"categories","in":"query","required":false,"description":"Catalogue categories, comma-separated, e.g. `removal,plant_health`. A POSITIVE filter: work with no catalogue-backed lines has no categories and does not match. Appointments sell nothing, so any value here excludes them.","schema":{"type":"array","items":{"type":"string"}}},{"name":"crewIds","in":"query","required":false,"description":"Crew ids, comma-separated. Applies to placed crew-days only; it never drops appointments, which have no crew.","schema":{"type":"array","items":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"}}},{"name":"assigneeIds","in":"query","required":false,"description":"User ids, comma-separated. Applies to appointments only.","schema":{"type":"array","items":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"}}},{"name":"minAgeDays","in":"query","required":false,"description":"Accepted at least this many WHOLE days ago. Work with no accepted date has no age and is excluded rather than counted as zero.","schema":{"type":"integer","minimum":0,"maximum":3650}},{"name":"acceptedFrom","in":"query","required":false,"description":"Accepted on or after this day.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},{"name":"acceptedTo","in":"query","required":false,"description":"Accepted on or before this day, inclusive.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},{"name":"scheduledFrom","in":"query","required":false,"description":"Placed on or after this day. Unplaced work is excluded: it is not on the calendar, so it is not an answer to a question about the calendar.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},{"name":"scheduledTo","in":"query","required":false,"description":"Placed on or before this day, inclusive.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},{"name":"dueOnly","in":"query","required":false,"description":"Only work whose next program round is due or overdue. A job that is not part of a program never matches.","schema":{"type":"string"}},{"name":"estimateIds","in":"query","required":false,"description":"Exactly these estimates, comma-separated. For a caller that already has the ids and wants their current state rather than a page of the queue.","schema":{"type":"array","items":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"}}},{"name":"districtIds","in":"query","required":false,"description":"District ids, comma-separated. Matching ANY of them is enough. A site can be in more than one district, and work whose site is in none never matches.","schema":{"type":"array","items":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"}}},{"name":"bbox","in":"query","required":false,"description":"An area, as `west,south,east,north` in degrees — the corners of a box on the map. Work with no coordinates is excluded, because a question about an area cannot be answered yes for something with no location. A box crossing the antimeridian (west > east) is supported.","schema":{"type":"string"}}],"responses":{"200":{"description":"List placed work","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"}},"content":{"application/json":{"schema":{"description":"Placed work, ordered by day and then by drive order.","type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"type":"object","properties":{"upcoming":{"type":"array","items":{"$ref":"#/components/schemas/ScheduledWork"}},"needsAttention":{"description":"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.","type":"array","items":{"$ref":"#/components/schemas/ScheduledWork"}}},"required":["upcoming","needsAttention"],"additionalProperties":false}},"required":["success","message","data"],"additionalProperties":false},"example":{"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,"serviceCategories":["pruning"],"districtIds":[],"siteId":"3a7c9e21-5b48-4f0a-9d33-71c6ee204b18","siteLabel":"3760 W Delap Rd","latitude":39.1912,"longitude":-86.5847,"totalCents":122500,"plannedHours":8,"crew":{"id":"d2f4a601-88b3-4c17-9e5a-3b7f0c1d2e94","name":"Crew A","color":"#2f855a","isArchived":false}}],"needsAttention":[]}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"operationId":"scheduleEstimates","summary":"Place accepted work on a crew and a day","description":"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. Resend the SAME key and body and the boundary replays the original answer byte for byte, with an `Idempotent-Replay: true` header and a 200 rather than the original status: that header is how you tell `it worked` from `it already worked`. Place the same estimate on the same day under a NEW key and its row reads `skipped`. Nothing is placed twice either way.\n\nRequires the `schedule:write` scope.","tags":["Schedule"],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"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.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Place accepted work on a crew and a day","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"},"Idempotent-Replay":{"description":"Present and `true` only when this response is a replay of an earlier request that carried the same `Idempotency-Key`. Nothing was created or changed to produce it.","schema":{"type":"string","enum":["true"]}}},"content":{"application/json":{"schema":{"description":"Per estimate, because a batch WILL half-succeed. The ones that landed stay landed, and the response is 200 even when some failed: read `items`, not the status code, to find out what happened to each one.","type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"type":"object","properties":{"placedCount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"failedCount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"items":{"type":"array","items":{"type":"object","properties":{"estimateId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"status":{"description":"`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.","type":"string","enum":["placed","skipped","failed"]},"error":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["estimateId","status","error"],"additionalProperties":false}}},"required":["placedCount","failedCount","items"],"additionalProperties":false}},"required":["success","message","data"],"additionalProperties":false},"example":{"success":true,"message":"Work scheduled","data":{"placedCount":1,"failedCount":0,"items":[{"estimateId":"e5d4c3b2-a190-4877-b6e5-d4c3b2a19087","status":"placed","error":null}]}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"estimateIds":{"description":"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.","minItems":1,"maxItems":200,"type":"array","items":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"}},"crewId":{"description":"The crew to place them on, or `null` to pencil the day in without one. Read `GET /crews` for the id.","anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$"},{"type":"null"}]},"scheduledDate":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"note":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}]},"plannedHours":{"description":"Hours to place on this day, per estimate. OMIT to place everything that is still unplaced, which is what this endpoint has always done. Pass a smaller number to fill a day and leave the rest: a sixteen-hour removal placed at 8 keeps eight hours unscheduled, and the estimate keeps appearing in work still to be placed until they are all on a day.","type":"number","exclusiveMinimum":0,"maximum":24},"lineItemId":{"description":"WHICH SERVICE these days cover. Required for a program: a round's wait is counted from the day the PREVIOUS round was worked, so a placement that does not say which round it was leaves the next one with nothing to count from and it never becomes due. Ignored unless exactly one estimate is being placed, because a sold line belongs to one estimate. Read the line ids from `GET /estimates/{id}`.","type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$"}},"required":["estimateIds","crewId","scheduledDate"]},"example":{"estimateIds":["e5d4c3b2-a190-4877-b6e5-d4c3b2a19087"],"crewId":"d2f4a601-88b3-4c17-9e5a-3b7f0c1d2e94","scheduledDate":"2026-09-08","note":"Ellettsville cluster, north to south"}}}}}},"/schedule/bump":{"post":{"operationId":"bumpCrewDay","summary":"Move a crew's whole day forward","description":"It rained. The day does not happen, and everything on it moves together. Unlike `POST /schedule`, which reports per estimate because a batch is a selection somebody made, this is ATOMIC: the whole day lands on the new date or nothing moves at all. A board showing half a crew tomorrow and half today is worse than one that did not move. `days` counts WORKING days, so 1 from a Friday is Monday. Read `toDate` off the response rather than computing it. THE BUMPED DAY IS ALWAYS EMPTY AFTERWARDS, and `movedCount` is everything that left it. One case does not land on `toDate`: a job spanning several days already holds that date, so its row steps on to the next free working day instead and comes back in `landedLater` with the date it actually reached. Report those separately, because a crew is going to that site on a different day from the rest of the load. Work that is finished or cancelled does not move: it is history. Send an `Idempotency-Key` and a retry replays the original answer instead of moving the day a second time. If a concurrent bump of an adjacent day lands work on your target while yours is running, nothing moves and you are told to try again.\n\nRequires the `schedule:write` scope.","tags":["Schedule"],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"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.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Move a crew's whole day forward","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"},"Idempotent-Replay":{"description":"Present and `true` only when this response is a replay of an earlier request that carried the same `Idempotency-Key`. Nothing was created or changed to produce it.","schema":{"type":"string","enum":["true"]}}},"content":{"application/json":{"schema":{"description":"What moved, where each of it went, and whether its order travelled.","type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"type":"object","properties":{"fromDate":{"type":"string"},"toDate":{"description":"Where the day landed. Compute it yourself only if you have to: this is the date the rows actually carry.","type":"string"},"movedCount":{"description":"Everything that left the bumped day, `landedLater` included. That day is always empty afterwards.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"landedLater":{"description":"Rows that moved, but not to `toDate`. A job spanning several days already holds the target date, so its row from the bumped day steps on to the next free working day instead. Report these separately: a crew is going to that site on a different day from the rest of the load.","type":"array","items":{"type":"object","properties":{"scheduleId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"jobId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"landedOn":{"description":"Where this one actually went.","type":"string"},"reason":{"type":"string","const":"job_already_had_that_day"}},"required":["scheduleId","jobId","landedOn","reason"],"additionalProperties":false}},"orderPinned":{"description":"True when the day you moved had a MANUAL running order and that order travelled with it. A pin is what holds an emergency at the front of a run, so a day carrying one arrives still pinned and the day it left is unpinned. Put it back on automatic with `POST /routes/{crewId}/{date}/automatic` when the day is normal.","type":"boolean"}},"required":["fromDate","toDate","movedCount","landedLater","orderPinned"],"additionalProperties":false}},"required":["success","message","data"],"additionalProperties":false},"example":{"success":true,"message":"Day moved","data":{"fromDate":"2026-09-11","toDate":"2026-09-14","movedCount":6,"landedLater":[],"orderPinned":false}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"crewId":{"description":"The crew whose day is moving. `null` moves the unassigned board, which is a real board a dispatcher reads on its own.","anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$"},{"type":"null"}]},"fromDate":{"description":"The day that is not happening, `YYYY-MM-DD`.","type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"days":{"description":"How many WORKING days forward. Weekends are skipped by counting, not by adding and nudging, so `1` from a Friday is Monday and `2` is Tuesday. Forward only: pulling work earlier is a different decision with different consequences, and nothing here does it.","type":"integer","minimum":1,"maximum":14}},"required":["crewId","fromDate","days"]},"example":{"crewId":"d2f4a601-88b3-4c17-9e5a-3b7f0c1d2e94","fromDate":"2026-09-11","days":1}}}}}},"/schedule/reassign":{"post":{"operationId":"reassignCrewDay","summary":"Hand a crew's whole day to another crew","description":"Somebody called in sick and the work still has to happen today. THE DATE DOES NOT CHANGE: this moves the crew, not the day. If the work should happen later instead, use `POST /schedule/bump`, and do not guess which one the person meant. Atomic, like the bump: the whole day changes hands or none of it does, because half a handover leaves two crews each believing the other has the afternoon. Either crew id may be `null` for the unassigned board, which is a real board on both sides. Arriving stops are appended after whatever the receiving crew already had, and both crew-days are re-routed. One case cannot move: a stop whose job ALREADY has the receiving crew that day has nowhere to go, since the date is fixed and merging the two rows would lose hours recorded per row. Those come back in `notMoved` and are STILL WITH THE ORIGINAL CREW, so do not report the handover as complete without naming them. Finished and cancelled work does not move. Send an `Idempotency-Key`.\n\nRequires the `schedule:write` scope.","tags":["Schedule"],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"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.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Hand a crew's whole day to another crew","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"},"Idempotent-Replay":{"description":"Present and `true` only when this response is a replay of an earlier request that carried the same `Idempotency-Key`. Nothing was created or changed to produce it.","schema":{"type":"string","enum":["true"]}}},"content":{"application/json":{"schema":{"description":"What changed hands, what could not, and whether its order travelled.","type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"type":"object","properties":{"date":{"type":"string"},"fromCrewId":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},{"type":"null"}]},"toCrewId":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},{"type":"null"}]},"movedCount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"notMoved":{"description":"Stops STILL WITH THE ORIGINAL CREW, and why. Somebody has to decide what happens to these, so do not report the handover as complete without naming them.\n\n`already_started`: a crew member is clocked in on that stop right now. They are at the site, so it is the one job on the day that does not need covering, and moving it would leave the timesheet and the board disagreeing about who worked it.\n\n`target_crew_already_on_that_job`: the receiving crew already has that job that day, and there is nowhere else for the row to go — the date is fixed, and folding the two together would lose hours, which are recorded per row.","type":"array","items":{"type":"object","properties":{"scheduleId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"jobId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"reason":{"type":"string","enum":["target_crew_already_on_that_job","already_started"]}},"required":["scheduleId","jobId","reason"],"additionalProperties":false}},"orderPinned":{"description":"True when the outgoing crew's day had a MANUAL running order and the receiving crew's day inherited it. Without that, an emergency the outgoing crew was going to first would be reordered by the optimizer the moment the day changed hands.","type":"boolean"}},"required":["date","fromCrewId","toCrewId","movedCount","notMoved","orderPinned"],"additionalProperties":false}},"required":["success","message","data"],"additionalProperties":false},"example":{"success":true,"message":"Day handed over","data":{"date":"2026-09-14","fromCrewId":"d2f4a601-88b3-4c17-9e5a-3b7f0c1d2e94","toCrewId":"9f1c7a20-5d84-4e13-9b62-3a7d5c8e4f10","movedCount":5,"notMoved":[],"orderPinned":false}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"description":"The date is the point. If the work should happen on a different day instead, that is `POST /schedule/bump`.","type":"object","properties":{"fromCrewId":{"description":"The crew that is not coming in. `null` is the unassigned board, which is a real board: work with a day but nobody on it yet.","anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$"},{"type":"null"}]},"toCrewId":{"description":"Who picks it up. `null` puts the day back on the unassigned board, which is what you do before you know who is covering.","anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$"},{"type":"null"}]},"date":{"description":"The day, `YYYY-MM-DD`. It does not change.","type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},"required":["fromCrewId","toCrewId","date"]},"example":{"fromCrewId":"d2f4a601-88b3-4c17-9e5a-3b7f0c1d2e94","toCrewId":"9f1c7a20-5d84-4e13-9b62-3a7d5c8e4f10","date":"2026-09-14"}}}}}},"/schedule/emergency":{"post":{"operationId":"insertEmergencyStop","summary":"A tree came down: put this job next","description":"Storm work. `estimateId` is the id `GET /schedule/unscheduled` gives you, so a row off the queue goes straight in. The job goes to the front of what a crew has left to do today, and the day is PINNED to manual so the optimizer cannot re-sort it back into the middle of the run. `position` is 1-based and is NOT always 1: a stop the crew has already clocked on to stays in front of it, because nothing can go before work somebody is standing on. If the job is already on that day this moves it up the order instead of adding a second row, and says so in `wasAlreadyOnTheDay`. THE PART TO READ IS `displaced`: the emergency is going to happen, so something else is not, and that list is which. Those jobs are still on the day and nobody has been told, so hand the list back for phone calls rather than treating them as cancelled. `orderPinned` means the day stays as you left it until somebody calls `POST /routes/{crewId}/{date}/automatic`. `crewId` may be null for the unassigned board. Send an `Idempotency-Key`.\n\nRequires the `schedule:write` scope.","tags":["Schedule"],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"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.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"A tree came down: put this job next","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"},"Idempotent-Replay":{"description":"Present and `true` only when this response is a replay of an earlier request that carried the same `Idempotency-Key`. Nothing was created or changed to produce it.","schema":{"type":"string","enum":["true"]}}},"content":{"application/json":{"schema":{"description":"Where it landed, and what it pushed off the end.","type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"type":"object","properties":{"scheduleId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"estimateId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"crewId":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},{"type":"null"}]},"date":{"type":"string"},"wasAlreadyOnTheDay":{"description":"True when the job was already on this day and only moved up the running order. Not a duplicate, and not an error: \"go there next instead\" is a real request.","type":"boolean"},"position":{"description":"1-based position in the running order. NOT always 1. Work the crew has already clocked on to stays in front, because nothing can go before a stop somebody is standing on.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"orderedScheduleIds":{"type":"array","items":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"}},"orderPinned":{"description":"The day is now `manual`, so the optimizer will not re-sort this stop out of position. False only for the unassigned board, which has no route to control. Switch it back with `POST /routes/{crewId}/{date}/automatic` once the day is normal again.","type":"boolean"},"displaced":{"description":"WORK THAT NO LONGER FITS THE DAY. The emergency is going to happen, so something else is not, and this is which. These jobs are still ON the day: nothing has been unscheduled and nobody has been told. Read this list out and hand it back, because those customers need a phone call.","type":"array","items":{"type":"object","properties":{"scheduleId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"estimateId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"jobNumber":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"addressLabel":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["scheduleId","estimateId","jobNumber","addressLabel"],"additionalProperties":false}},"dayEndsAtMinutes":{"description":"When the last stop now finishes, minutes past midnight. `null` when the day carries no estimates at all, so that any claim about fitting would be a guess rather than a measurement.","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]}},"required":["scheduleId","estimateId","crewId","date","wasAlreadyOnTheDay","position","orderedScheduleIds","orderPinned","displaced","dayEndsAtMinutes"],"additionalProperties":false}},"required":["success","message","data"],"additionalProperties":false},"example":{"success":true,"message":"Scheduled next","data":{"scheduleId":"b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e","estimateId":"e5d4c3b2-a190-4877-b6e5-d4c3b2a19087","crewId":"d2f4a601-88b3-4c17-9e5a-3b7f0c1d2e94","date":"2026-09-14","wasAlreadyOnTheDay":false,"position":1,"orderedScheduleIds":["b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e"],"orderPinned":true,"displaced":[],"dayEndsAtMinutes":1005}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"estimateId":{"description":"The work that has to happen now. This is the `estimateId` that `GET /schedule/unscheduled` and `GET /estimates` return, so a row off the queue can be sent here unchanged.","type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"crewId":{"description":"Who goes. `null` puts it on the unassigned board: somebody has to go and who is a second decision, which should not slow this down.","anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$"},{"type":"null"}]},"date":{"description":"The day, `YYYY-MM-DD`.","type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"plannedHours":{"description":"How long it will take. Omit or `null` when nobody knows yet, which is the normal state of a storm call taken over the phone. The day's arithmetic then falls back to a placeholder and says so.","anyOf":[{"type":"number","exclusiveMinimum":0,"maximum":24},{"type":"null"}]},"note":{"description":"What happened, for whoever reads the day. \"Tree on the house\" is the kind of thing a crew needs to see before they arrive.","anyOf":[{"type":"string","maxLength":500},{"type":"null"}]}},"required":["estimateId","crewId","date"]},"example":{"estimateId":"e5d4c3b2-a190-4877-b6e5-d4c3b2a19087","crewId":"d2f4a601-88b3-4c17-9e5a-3b7f0c1d2e94","date":"2026-09-14","plannedHours":3.5,"note":"Tree on the house"}}}}}},"/schedule/unscheduled":{"get":{"operationId":"listWorkToPlace","summary":"List work waiting for a day","description":"Agreed work that still has hours nobody has given a day to. This is the queue scheduling actually works from: an estimate lands here the moment it is accepted, and leaves when its hours are all placed. Paged rather than capped, because filtering a list that was silently truncated gives a confident wrong answer: `total` is the size of the whole set, so page with `offset` until you have seen it. Every field you would want to filter on is published, including `serviceCategories`, `acceptedAt`, `districtIds` and the site coordinates — and every one of them can also be passed as a query parameter, so you can narrow the set server-side and page through exactly what you asked for rather than filtering a page after it arrives. To find out WHERE a row should go, pass its id to `GET /schedule/fit`, which ranks every (crew, day) with a reason instead of returning only the top one.\n\nRequires the `schedule:read` scope.","tags":["Schedule"],"parameters":[{"name":"limit","in":"query","required":false,"description":"Rows per page, 1 to 200.","schema":{"default":50,"type":"integer","minimum":1,"maximum":200}},{"name":"offset","in":"query","required":false,"description":"Rows to skip. Page with this until you have `total` rows: the order is total (oldest acceptance first, then estimate number), so pages do not repeat or skip.","schema":{"default":0,"type":"integer","minimum":0,"maximum":9007199254740991}},{"name":"q","in":"query","required":false,"description":"Free text over the site label and the job number. NOT the customer: unplaced work carries no customer name, and promising one here would be a search that silently never matches.","schema":{"type":"string","maxLength":200}},{"name":"kinds","in":"query","required":false,"description":"Which kinds to include, comma-separated. Omit for all three. `work_to_place` is accepted work with no day, `work_day` is a placed crew-day, `appointment` is a visit.","schema":{"type":"array","items":{"type":"string","enum":["work_to_place","work_day","appointment"]}}},{"name":"categories","in":"query","required":false,"description":"Catalogue categories, comma-separated, e.g. `removal,plant_health`. A POSITIVE filter: work with no catalogue-backed lines has no categories and does not match. Appointments sell nothing, so any value here excludes them.","schema":{"type":"array","items":{"type":"string"}}},{"name":"crewIds","in":"query","required":false,"description":"Crew ids, comma-separated. Applies to placed crew-days only; it never drops appointments, which have no crew.","schema":{"type":"array","items":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"}}},{"name":"assigneeIds","in":"query","required":false,"description":"User ids, comma-separated. Applies to appointments only.","schema":{"type":"array","items":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"}}},{"name":"minAgeDays","in":"query","required":false,"description":"Accepted at least this many WHOLE days ago. Work with no accepted date has no age and is excluded rather than counted as zero.","schema":{"type":"integer","minimum":0,"maximum":3650}},{"name":"acceptedFrom","in":"query","required":false,"description":"Accepted on or after this day.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},{"name":"acceptedTo","in":"query","required":false,"description":"Accepted on or before this day, inclusive.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},{"name":"scheduledFrom","in":"query","required":false,"description":"Placed on or after this day. Unplaced work is excluded: it is not on the calendar, so it is not an answer to a question about the calendar.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},{"name":"scheduledTo","in":"query","required":false,"description":"Placed on or before this day, inclusive.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},{"name":"dueOnly","in":"query","required":false,"description":"Only work whose next program round is due or overdue. A job that is not part of a program never matches.","schema":{"type":"string"}},{"name":"estimateIds","in":"query","required":false,"description":"Exactly these estimates, comma-separated. For a caller that already has the ids and wants their current state rather than a page of the queue.","schema":{"type":"array","items":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"}}},{"name":"districtIds","in":"query","required":false,"description":"District ids, comma-separated. Matching ANY of them is enough. A site can be in more than one district, and work whose site is in none never matches.","schema":{"type":"array","items":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"}}},{"name":"bbox","in":"query","required":false,"description":"An area, as `west,south,east,north` in degrees — the corners of a box on the map. Work with no coordinates is excluded, because a question about an area cannot be answered yes for something with no location. A box crossing the antimeridian (west > east) is supported.","schema":{"type":"string"}}],"responses":{"200":{"description":"List work waiting for a day","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"}},"content":{"application/json":{"schema":{"description":"Agreed work that still has hours nobody has given a day to.","type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"estimateId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"estimateNumber":{"description":"Formatted, e.g. \"JOB-0062\".","type":"string"},"siteId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"siteLabel":{"description":"The site's nickname if it has one, otherwise its street.","anyOf":[{"type":"string"},{"type":"null"}]},"acceptedAt":{"description":"When the customer said yes. The only urgency signal this list carries: sort by it to find what has been waiting longest.","anyOf":[{"type":"string"},{"type":"null"}]},"estimatedHours":{"description":"How long the work is expected to take, when somebody has estimated it. `null` means nobody has. It does NOT mean zero, and a planner that reads it as zero will overfill a day.","anyOf":[{"type":"number"},{"type":"null"}]},"remainingHours":{"description":"How much of `estimatedHours` is still unplaced. Equal to it for work with no days yet, smaller once some has been placed, `null` when there is nothing to subtract from.","anyOf":[{"type":"number"},{"type":"null"}]},"expectedDays":{"description":"Duration hint, not a constraint.","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"latitude":{"description":"`null` means the site was never geocoded. It is listed anyway.","anyOf":[{"type":"number"},{"type":"null"}]},"longitude":{"anyOf":[{"type":"number"},{"type":"null"}]},"serviceCategories":{"description":"Distinct catalogue categories across this estimate's lines, so the list can be filtered by what the work actually is. Empty is legal and means no catalogue-backed lines, never `no services`.","type":"array","items":{"type":"string"}},"districtIds":{"description":"Every named area this estimate's SITE falls in. A site can be in more than one: districts are a filing convention, not a partition. Empty when the site has no coordinates or falls outside every district. Returned as well as filterable, so a `districtIds` filter can be checked against the rows it gives back.","type":"array","items":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"}},"dueRound":{"description":"The program round whose window is open. Null on ordinary work, which is most of it. A round becomes due a set number of days after the PREVIOUS round was worked, so this is the field that tells you a six-visit plant health care plan needs its next visit booked. `dueOnly=true` filters to exactly the rows where this is not null.","anyOf":[{"type":"object","properties":{"description":{"description":"The service, e.g. `Round 2 - Spring foliar`.","type":"string"},"status":{"type":"string","enum":["due","overdue"]},"earliestDate":{"description":"`YYYY-MM-DD`.","anyOf":[{"type":"string"},{"type":"null"}]},"latestDate":{"description":"Null on an open-ended wait: at least N days, no deadline.","anyOf":[{"type":"string"},{"type":"null"}]}},"required":["description","status","earliestDate","latestDate"],"additionalProperties":false},{"type":"null"}]}},"required":["estimateId","estimateNumber","siteId","siteLabel","acceptedAt","estimatedHours","remainingHours","expectedDays","latitude","longitude","serviceCategories","districtIds","dueRound"],"additionalProperties":false}},"total":{"description":"The size of the WHOLE set, not of this page. Filter against this so you know whether you have seen all of it.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"nextOffset":{"description":"Pass as `?offset=`. `null` means this was the last page.","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]}},"required":["items","total","nextOffset"],"additionalProperties":false}},"required":["success","message","data"],"additionalProperties":false},"example":{"success":true,"message":"Work to place retrieved","data":{"items":[{"estimateId":"e5d4c3b2-a190-4877-b6e5-d4c3b2a19087","estimateNumber":"JOB-0062","siteId":"3a7c9e21-5b48-4f0a-9d33-71c6ee204b18","siteLabel":"3760 W Delap Rd","acceptedAt":"2026-09-01T01:47:21.570Z","estimatedHours":6,"remainingHours":6,"expectedDays":1,"latitude":39.1912,"longitude":-86.5847,"serviceCategories":["pruning"],"districtIds":[],"dueRound":null}],"total":14,"nextOffset":null}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/districts":{"get":{"operationId":"listDistricts","summary":"List the named areas","description":"The areas this org files work under. `districtIds` is a filter on the schedule endpoints, and this is where its values come from: a filter whose values cannot be looked up is a filter nobody can use. A site can fall in more than one district — they are a filing convention, not a partition — and work whose site has no coordinates falls in none.\n\nRequires the `schedule:read` scope.","tags":["Schedule"],"parameters":[],"responses":{"200":{"description":"List the named areas","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"}},"content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"name":{"type":"string"},"boundary":{"description":"A closed ring of `[longitude, latitude]` pairs, GeoJSON axis order. The ring closes implicitly, so the first point is not repeated.","type":"array","items":{"type":"array","prefixItems":[{"type":"number"},{"type":"number"}]}},"createdAt":{"type":"string"}},"required":["id","name","boundary","createdAt"],"additionalProperties":false}}},"required":["items"],"additionalProperties":false}},"required":["success","message","data"],"additionalProperties":false},"example":{"success":true,"message":"Districts retrieved","data":{"items":[{"id":"7c2f5a91-3e64-4b7d-8a15-2f9c6d4e8b03","name":"North Valley","boundary":[[-115.4,36.1],[-115,36.1],[-115,36.35],[-115.4,36.35]],"createdAt":"2026-09-03T04:00:00.000Z"}]}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"operationId":"createDistrict","summary":"Draw a named area","description":"Give an area a name so work in it can be pulled by that name. The boundary is a closed ring of `[longitude, latitude]` points and the ring closes implicitly, so the four corners of a bounding box are a valid district: the same `bbox` you filter with can be saved as one. Three points is the minimum, because two is a line and a line contains nothing. Names are unique per org, since two districts called `North` is a filter nobody can read.\n\nRequires the `schedule:write` scope.","tags":["Schedule"],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"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.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Draw a named area (replay of an earlier request with the same `Idempotency-Key`; nothing was created)","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"},"Idempotent-Replay":{"description":"Present and `true` only when this response is a replay of an earlier request that carried the same `Idempotency-Key`. Nothing was created or changed to produce it.","schema":{"type":"string","enum":["true"]}}},"content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"name":{"type":"string"},"boundary":{"description":"A closed ring of `[longitude, latitude]` pairs, GeoJSON axis order. The ring closes implicitly, so the first point is not repeated.","type":"array","items":{"type":"array","prefixItems":[{"type":"number"},{"type":"number"}]}},"createdAt":{"type":"string"}},"required":["id","name","boundary","createdAt"],"additionalProperties":false}},"required":["success","message","data"],"additionalProperties":false},"example":{"success":true,"message":"District created","data":{"id":"7c2f5a91-3e64-4b7d-8a15-2f9c6d4e8b03","name":"North Valley","boundary":[[-115.4,36.1],[-115,36.1],[-115,36.35],[-115.4,36.35]],"createdAt":"2026-09-03T04:00:00.000Z"}}}}},"201":{"description":"Draw a named area","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"},"Idempotent-Replay":{"description":"Present and `true` only when this response is a replay of an earlier request that carried the same `Idempotency-Key`. Nothing was created or changed to produce it.","schema":{"type":"string","enum":["true"]}}},"content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"name":{"type":"string"},"boundary":{"description":"A closed ring of `[longitude, latitude]` pairs, GeoJSON axis order. The ring closes implicitly, so the first point is not repeated.","type":"array","items":{"type":"array","prefixItems":[{"type":"number"},{"type":"number"}]}},"createdAt":{"type":"string"}},"required":["id","name","boundary","createdAt"],"additionalProperties":false}},"required":["success","message","data"],"additionalProperties":false},"example":{"success":true,"message":"District created","data":{"id":"7c2f5a91-3e64-4b7d-8a15-2f9c6d4e8b03","name":"North Valley","boundary":[[-115.4,36.1],[-115,36.1],[-115,36.35],[-115.4,36.35]],"createdAt":"2026-09-03T04:00:00.000Z"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"description":"Unique within the org.","type":"string","minLength":1,"maxLength":80},"boundary":{"description":"At least three `[longitude, latitude]` points. Three is the fewest that is an area; two is a line and a line contains nothing.","minItems":3,"type":"array","items":{"type":"array","prefixItems":[{"type":"number"},{"type":"number"}]}}},"required":["name","boundary"],"additionalProperties":false},"example":{"name":"North Valley","boundary":[[-115.4,36.1],[-115,36.1],[-115,36.35],[-115.4,36.35]]}}}}}},"/schedule/fit":{"get":{"operationId":"rankCapacityFits","summary":"Where would this work fit","description":"Rank (crew, day) pairs for work you are about to place, with the reason for each. This is the question a dispatcher actually asks, and it was previously answerable only by a person looking at the board: you could read the backlog and read the schedule and still not know where to put anything. Pass `estimateIds` rather than `hours` wherever you can, because it takes the size from the estimates themselves including the fallback this product uses for work nobody has estimated. Order is days it fits, nearest first, then soonest: proximity leads because the real cost is windshield time, and a day with room across town is worse than a slightly tighter day four minutes away. Days it does NOT fit are ranked last and never dropped, because `nothing fits` is an answer. **This endpoint ranks and never places.** It has no side effects; placing one of these is a separate, deliberate `POST /schedule`. A wrong ranking costs a second look, a wrong placement costs somebody’s Tuesday.\n\nRequires the `schedule:read` scope.","tags":["Schedule"],"parameters":[{"name":"from","in":"query","required":false,"description":"First day to rank. Defaults to today, UTC.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},{"name":"days","in":"query","required":false,"description":"How many WORKING days forward to rank. Weekends are skipped, so 10 days reaches a fortnight out.","schema":{"default":10,"type":"integer","minimum":1,"maximum":30}},{"name":"estimateIds","in":"query","required":false,"description":"The unplaced work you are trying to place, comma-separated. PREFER THIS over `hours`: it takes the size from the estimates themselves, including this product's own fallback for work nobody has estimated, which you would otherwise have to reproduce. An id that is not waiting to be placed is a 404, not an empty ranking.","schema":{"type":"array","items":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"}}},{"name":"hours","in":"query","required":false,"description":"How long the work takes, for reasoning about work that does not exist yet. Ignored when `estimateIds` is given.","schema":{"type":"number","exclusiveMinimum":0,"maximum":999}},{"name":"lat","in":"query","required":false,"schema":{"type":"number","minimum":-90,"maximum":90}},{"name":"lng","in":"query","required":false,"description":"Where the work is, so days near existing stops rank higher. Without it every day is judged on room alone, which is a worse answer, not an error.","schema":{"type":"number","minimum":-180,"maximum":180}},{"name":"crewIds","in":"query","required":false,"description":"Only rank these crews. Omit for all of them.","schema":{"type":"array","items":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"}}}],"responses":{"200":{"description":"Where would this work fit","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"}},"content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"type":"object","properties":{"fits":{"description":"Best first: days it fits, nearest first, then soonest. Proximity leads because the real cost is windshield time. Days it does NOT fit are ranked last and never dropped, because `nothing fits` is an answer.","type":"array","items":{"type":"object","properties":{"crewId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"crewName":{"type":"string"},"date":{"description":"`YYYY-MM-DD`.","type":"string"},"fits":{"description":"Whether the work finishes inside this crew's own working day. False does NOT mean refused: placing it is still allowed and the API will accept it. It means somebody is working late.","type":"boolean"},"remainingMinutes":{"description":"Room left AFTER this work. Negative means it runs over.","type":"number"},"nearestMeters":{"description":"Crow-flies distance to that crew-day's nearest existing stop. `null` when the day is empty: an empty day has room but no gravity, so it can never be `on the way` to anything.","anyOf":[{"type":"number"},{"type":"null"}]},"bookedMinutes":{"description":"How full the day already is.","type":"number"},"stops":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"reason":{"description":"The same sentence the dispatcher's board shows, so an agent and a human are told the same thing in the same words.","type":"string"}},"required":["crewId","crewName","date","fits","remainingMinutes","nearestMeters","bookedMinutes","stops","reason"],"additionalProperties":false}},"need":{"type":"object","properties":{"minutes":{"type":"number"},"estimated":{"description":"False when the size is this product's fallback rather than somebody's estimate. Plan around a false here and you are planning around a number nobody entered.","type":"boolean"}},"required":["minutes","estimated"],"additionalProperties":false}},"required":["fits","need"],"additionalProperties":false}},"required":["success","message","data"],"additionalProperties":false},"example":{"success":true,"message":"Capacity ranked","data":{"fits":[{"crewId":"0f9d2c1e-6b3a-4d5e-9f21-7c8b4a1d2e35","crewName":"Tree Crew","date":"2026-09-10","fits":true,"remainingMinutes":144,"nearestMeters":3219,"bookedMinutes":336,"stops":3,"reason":"2.4h would be left, nearest stop 2 mi"}],"need":{"minutes":240,"estimated":true}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/estimates/{estimateId}/schedule/{scheduleId}":{"patch":{"operationId":"reschedulePlacement","summary":"Move a placement","description":"Change the day, the crew, or the note on one placement, in place. Send only what you are changing: omitting `crewId` leaves the crew alone and sending `null` takes the work off a crew, and those are different requests. A placement that would collide with the same estimate already being on that crew's day is refused and nothing moves.\n\nRequires the `schedule:write` scope.","tags":["Schedule"],"parameters":[{"name":"estimateId","in":"path","required":true,"description":"The estimate the placement belongs to.","schema":{"type":"string","format":"uuid"}},{"name":"scheduleId","in":"path","required":true,"description":"The placement, from `GET /schedule`.","schema":{"type":"string","format":"uuid"}},{"name":"Idempotency-Key","in":"header","required":false,"description":"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.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Move a placement","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"},"Idempotent-Replay":{"description":"Present and `true` only when this response is a replay of an earlier request that carried the same `Idempotency-Key`. Nothing was created or changed to produce it.","schema":{"type":"string","enum":["true"]}}},"content":{"application/json":{"schema":{"description":"One placement.","type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"type":"object","properties":{"scheduleId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"estimateId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"crewId":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},{"type":"null"}]},"scheduledDate":{"type":"string"},"sortOrder":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"note":{"anyOf":[{"type":"string"},{"type":"null"}]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["scheduleId","estimateId","crewId","scheduledDate","sortOrder","note","createdAt","updatedAt"],"additionalProperties":false}},"required":["success","message","data"],"additionalProperties":false},"example":{"success":true,"message":"Placement moved","data":{"scheduleId":"a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d","estimateId":"e5d4c3b2-a190-4877-b6e5-d4c3b2a19087","crewId":"d2f4a601-88b3-4c17-9e5a-3b7f0c1d2e94","scheduledDate":"2026-09-09","sortOrder":1000,"note":null,"createdAt":"2026-09-01T04:10:02.118Z","updatedAt":"2026-09-01T04:31:55.204Z"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"description":"At least one field. A patch that changes nothing is refused rather than answered, so a caller never reads a 200 as proof a move happened.","type":"object","properties":{"scheduledDate":{"description":"Move it to this day. Omit to leave the day alone.","type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"crewId":{"description":"Hand it to this crew, or `null` to take it off a crew and leave it pencilled on the day. Omit to leave the crew alone. The three are different, so omitting and sending `null` do NOT mean the same thing.","anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$"},{"type":"null"}]},"note":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}]}}},"example":{"scheduledDate":"2026-09-09","crewId":"d2f4a601-88b3-4c17-9e5a-3b7f0c1d2e94"}}}}},"delete":{"operationId":"unschedulePlacement","summary":"Take work off the board","description":"Removes one placement. The estimate is not touched and nothing is cancelled. If it was the last placement the estimate holds, the estimate goes back to `accepted` and `revertedToAccepted` says so, which means it reappears in `GET /schedule/unscheduled` waiting for a day.\n\nRequires the `schedule:write` scope.","tags":["Schedule"],"parameters":[{"name":"estimateId","in":"path","required":true,"description":"The estimate the placement belongs to.","schema":{"type":"string","format":"uuid"}},{"name":"scheduleId","in":"path","required":true,"description":"The placement to remove.","schema":{"type":"string","format":"uuid"}},{"name":"Idempotency-Key","in":"header","required":false,"description":"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.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Take work off the board","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"},"Idempotent-Replay":{"description":"Present and `true` only when this response is a replay of an earlier request that carried the same `Idempotency-Key`. Nothing was created or changed to produce it.","schema":{"type":"string","enum":["true"]}}},"content":{"application/json":{"schema":{"description":"The placement is gone. The estimate is not.","type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"type":"object","properties":{"removed":{"type":"boolean","const":true},"revertedToAccepted":{"description":"`true` when that was the estimate's last placement and it went back to `accepted`. It is agreed work again, waiting for a day, and it reappears in `GET /schedule/unscheduled`.","type":"boolean"}},"required":["removed","revertedToAccepted"],"additionalProperties":false}},"required":["success","message","data"],"additionalProperties":false},"example":{"success":true,"message":"Placement removed","data":{"removed":true,"revertedToAccepted":true}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/routes/{crewId}/{date}":{"get":{"operationId":"getCrewDayRoute","summary":"Get a crew's day in drive order","description":"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.\n\nRequires the `routing:read` scope.","tags":["Routes"],"parameters":[{"name":"crewId","in":"path","required":true,"description":"The crew, from `GET /crews`.","schema":{"type":"string","format":"uuid"}},{"name":"date","in":"path","required":true,"description":"The day, `YYYY-MM-DD`.","schema":{"type":"string","format":"date","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}}],"responses":{"200":{"description":"Get a crew's day in drive order","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"}},"content":{"application/json":{"schema":{"description":"One crew's day, in drive order.","type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"type":"object","properties":{"crewId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"date":{"description":"`YYYY-MM-DD`.","type":"string"},"status":{"description":"`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.","type":"string","enum":["not_configured","not_needed","pending","optimizing","ready","needs_attention","failed","stale","frozen"]},"optimizedAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"distanceMeters":{"anyOf":[{"type":"number"},{"type":"null"}]},"driveDurationSeconds":{"anyOf":[{"type":"number"},{"type":"null"}]},"orderMode":{"description":"`manual` means somebody pinned the order and the optimizer will not move it until `POST .../automatic` hands control back.","type":"string","enum":["automatic","manual"]},"orderSource":{"description":"Where the order in `orderedScheduleIds` actually came from. `schedule` means nobody optimized anything and this is simply the order the work was placed in.","type":"string","enum":["optimized","manual","schedule"]},"controlVersion":{"description":"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.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"orderedScheduleIds":{"description":"The drive order. Always populated, whatever the status.","type":"array","items":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"}},"legs":{"type":"array","items":{"type":"object","properties":{"from":{"type":"object","properties":{"type":{"type":"string","enum":["origin","stop"]},"label":{"anyOf":[{"type":"string"},{"type":"null"}]},"scheduleId":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},{"type":"null"}]}},"required":["type","label","scheduleId"],"additionalProperties":false},"to":{"type":"object","properties":{"type":{"type":"string","enum":["origin","stop"]},"label":{"anyOf":[{"type":"string"},{"type":"null"}]},"scheduleId":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},{"type":"null"}]}},"required":["type","label","scheduleId"],"additionalProperties":false},"distanceMeters":{"type":"number"},"driveDurationSeconds":{"type":"number"}},"required":["from","to","distanceMeters","driveDurationSeconds"],"additionalProperties":false}},"geometry":{"description":"Google's encoded polyline for the whole route, when there is one.","anyOf":[{"type":"object","properties":{"encodedPolyline":{"type":"string"},"precision":{"anyOf":[{"type":"number","const":5},{"type":"number","const":6}]}},"required":["encodedPolyline","precision"],"additionalProperties":false},{"type":"null"}]},"origin":{"description":"Where the day starts and, unless configured otherwise, ends.","anyOf":[{"type":"object","properties":{"label":{"type":"string"},"lat":{"type":"number"},"lng":{"type":"number"}},"required":["label","lat","lng"],"additionalProperties":false},{"type":"null"}]},"issues":{"description":"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.","type":"array","items":{"type":"object","properties":{"code":{"type":"string","enum":["missing_coordinate","multi_location","provider_unavailable"]},"scheduleId":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},{"type":"null"}]}},"required":["code","scheduleId"],"additionalProperties":false}}},"required":["crewId","date","status","optimizedAt","distanceMeters","driveDurationSeconds","orderMode","orderSource","controlVersion","orderedScheduleIds","legs","geometry","origin","issues"],"additionalProperties":false}},"required":["success","message","data"],"additionalProperties":false},"example":{"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":[]}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/routes/{crewId}/{date}/optimize":{"post":{"operationId":"optimizeCrewDayRoute","summary":"Ask for the day to be re-optimized","description":"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.\n\nRequires the `routing:write` scope.","tags":["Routes"],"parameters":[{"name":"crewId","in":"path","required":true,"description":"The crew.","schema":{"type":"string","format":"uuid"}},{"name":"date","in":"path","required":true,"description":"The day, `YYYY-MM-DD`.","schema":{"type":"string","format":"date","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},{"name":"Idempotency-Key","in":"header","required":false,"description":"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.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Ask for the day to be re-optimized","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"},"Idempotent-Replay":{"description":"Present and `true` only when this response is a replay of an earlier request that carried the same `Idempotency-Key`. Nothing was created or changed to produce it.","schema":{"type":"string","enum":["true"]}}},"content":{"application/json":{"schema":{"description":"An optimization was requested.","type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"type":"object","properties":{"status":{"description":"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`.","type":"string","const":"pending"}},"required":["status"],"additionalProperties":false}},"required":["success","message","data"],"additionalProperties":false},"example":{"success":true,"message":"Optimization requested","data":{"status":"pending"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/routes/{crewId}/{date}/order":{"post":{"operationId":"setCrewDayRouteOrder","summary":"Set the drive order by hand","description":"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.\n\nRequires the `routing:write` scope.","tags":["Routes"],"parameters":[{"name":"crewId","in":"path","required":true,"description":"The crew.","schema":{"type":"string","format":"uuid"}},{"name":"date","in":"path","required":true,"description":"The day, `YYYY-MM-DD`.","schema":{"type":"string","format":"date","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},{"name":"Idempotency-Key","in":"header","required":false,"description":"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.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Set the drive order by hand","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"},"Idempotent-Replay":{"description":"Present and `true` only when this response is a replay of an earlier request that carried the same `Idempotency-Key`. Nothing was created or changed to produce it.","schema":{"type":"string","enum":["true"]}}},"content":{"application/json":{"schema":{"description":"The day's order control after the change.","type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"type":"object","properties":{"orderMode":{"type":"string","enum":["automatic","manual"]},"controlVersion":{"description":"The NEW version. Read this one back for your next write.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"orderedScheduleIds":{"type":"array","items":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"}}},"required":["orderMode","controlVersion","orderedScheduleIds"],"additionalProperties":false}},"required":["success","message","data"],"additionalProperties":false},"example":{"success":true,"message":"Order set","data":{"orderMode":"manual","controlVersion":4,"orderedScheduleIds":["b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e","a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"]}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"expectedVersion":{"description":"The `controlVersion` you read. A stale one is refused with 409 rather than overwriting whatever changed in between.","type":"integer","minimum":0,"maximum":9007199254740991},"orderedScheduleIds":{"description":"Every placement on the day, in the order you want them driven. A partial list is refused: the day is reordered as a whole.","minItems":1,"maxItems":200,"type":"array","items":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"}}},"required":["expectedVersion","orderedScheduleIds"]},"example":{"expectedVersion":3,"orderedScheduleIds":["b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e","a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"]}}}}}},"/routes/{crewId}/{date}/automatic":{"post":{"operationId":"useAutomaticCrewDayRouteOrder","summary":"Hand the day back to the optimizer","description":"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.\n\nRequires the `routing:write` scope.","tags":["Routes"],"parameters":[{"name":"crewId","in":"path","required":true,"description":"The crew.","schema":{"type":"string","format":"uuid"}},{"name":"date","in":"path","required":true,"description":"The day, `YYYY-MM-DD`.","schema":{"type":"string","format":"date","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},{"name":"Idempotency-Key","in":"header","required":false,"description":"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.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Hand the day back to the optimizer","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"},"Idempotent-Replay":{"description":"Present and `true` only when this response is a replay of an earlier request that carried the same `Idempotency-Key`. Nothing was created or changed to produce it.","schema":{"type":"string","enum":["true"]}}},"content":{"application/json":{"schema":{"description":"The day's order control after the change.","type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"type":"object","properties":{"orderMode":{"type":"string","enum":["automatic","manual"]},"controlVersion":{"description":"The NEW version. Read this one back for your next write.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"orderedScheduleIds":{"type":"array","items":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"}}},"required":["orderMode","controlVersion","orderedScheduleIds"],"additionalProperties":false}},"required":["success","message","data"],"additionalProperties":false},"example":{"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"]}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"expectedVersion":{"description":"The `controlVersion` you read.","type":"integer","minimum":0,"maximum":9007199254740991}},"required":["expectedVersion"]},"example":{"expectedVersion":4}}}}}},"/sites/{siteId}/report":{"get":{"operationId":"getSiteReport","summary":"Get a site's inventory report","description":"A GeoJSON FeatureCollection of the site's mapped trees. Trees with no usable GPS fix are counted in `excludedCount` rather than dropped silently.\n\nRequires the `reports:read` scope.","tags":["Reports"],"parameters":[{"name":"siteId","in":"path","required":true,"description":"The site's id.","schema":{"type":"string","format":"uuid"}},{"name":"species","in":"query","required":false,"description":"Comma-separated species slugs. Omit for every species.","schema":{"type":"string","minLength":1}}],"responses":{"200":{"description":"Get a site's inventory report","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimit-Limit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimit-Remaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimit-Reset"}},"content":{"application/json":{"schema":{"description":"The site's mapped inventory as GeoJSON.","type":"object","properties":{"success":{"type":"boolean","const":true},"message":{"type":"string"},"data":{"type":"object","properties":{"type":{"type":"string","const":"FeatureCollection"},"features":{"type":"array","items":{"type":"object","properties":{},"additionalProperties":{}}},"excludedCount":{"description":"Trees omitted because they carry no usable GPS fix.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["type","features","excludedCount"],"additionalProperties":false}},"required":["success","message","data"],"additionalProperties":false},"example":{"success":true,"message":"Report retrieved","data":{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[-86.58471,39.19124]},"properties":{"treeNumber":1,"commonName":"Northern Red Oak","dbhInches":22,"healthCondition":"fair"}}],"excludedCount":0}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}}}}}