Estimates
Quotes. This is the only write that creates real money-shaped work, so it is the one to send an Idempotency-Key with.
post
https://treeinventory.ai/api/v1/estimatesCreate an estimate
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.
Scope: estimates:write
Headers
| Field | Type | Notes |
|---|---|---|
| Idempotency-Key | uuid | A UUID you mint per logical action and REUSE on every retry of it. Replay returns the original response and creates nothing. Omit it and a retry creates a second record. |
Request body
| Field | Type | Notes |
|---|---|---|
| siteIdrequired | uuid | |
| linesrequired | object[] |
Request body → lines
| Field | Type | Notes |
|---|---|---|
| catalogItemIdrequired | uuid | From GET /catalog/items. Inventing one is a 404. |
| description | string | |
| quantityrequired | number | |
| unitPriceCentsrequired | integer |
Example request
{
"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
}
]
}data
| Field | Type | Notes |
|---|---|---|
| id | uuid | |
| number | string | "JOB-0007", which is what the customer sees. |
| status | string | |
| siteId | uuid | |
| customerId | uuid or null | |
| origin | string | |
| totalCents | integer | |
| unpricedCount | integer | Non-zero means this estimate cannot be sent as-is. |
| narrative | string or null | |
| flags | string[] | |
| expiresAt | string or null | |
| sentAt | string or null | |
| acceptedAt | string or null | |
| lines | EstimateLine[] | |
| createdAt | string | |
| updatedAt | string |
data → lines
| Field | Type | Notes |
|---|---|---|
| id | uuid | |
| lineNumber | integer | |
| description | string or null | |
| coverage | string or null | What this line covers, as it appears on the customer document. |
| quantity | number | |
| unitPriceCents | integer or null | |
| amountCents | integer or null | Null means UNPRICED (no rate on file). It is not zero, and treating it as zero publishes free work. |
| status | string | |
| catalogItemId | uuid or null | |
| siteId | uuid or null | |
| treeIds | uuid[] | Which captured trees this line prices. |
Example response 201
{
"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": "3a7c9e21-5b48-4f0a-9d33-71c6ee204b18",
"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": "3a7c9e21-5b48-4f0a-9d33-71c6ee204b18",
"treeIds": []
}
],
"createdAt": "2026-08-25T18:22:41.771Z",
"updatedAt": "2026-08-25T18:22:41.771Z"
}
}Failures
401Missing, malformed, revoked or expired credential. TheWWW-Authenticateheader names the scheme, which isApiKey.403The key is valid but does not carry the scope this operation requires. The message names the scope, so you can ask for exactly the grant you need.404No such record in your organization. A record belonging to somebody else is indistinguishable from one that does not exist.409Idempotency conflict: thisIdempotency-Keywas used with a different body (idempotency_key_reused), or an identical request is still in flight (command_in_progress, withRetry-After).422Validation failed. Every bad field is named, at once.429Rate limit exceeded.500Something failed on our side. Retry with the SAME Idempotency-Key; a 5xx is the one status where retrying is correct.
get
https://treeinventory.ai/api/v1/estimates/{estimateId}Get one estimate
The read that makes a write checkable.
Scope: estimates:read
Path parameters
| Field | Type | Notes |
|---|---|---|
| estimateIdrequired | uuid | The estimate's id. |
data
| Field | Type | Notes |
|---|---|---|
| id | uuid | |
| number | string | "JOB-0007", which is what the customer sees. |
| status | string | |
| siteId | uuid | |
| customerId | uuid or null | |
| origin | string | |
| totalCents | integer | |
| unpricedCount | integer | Non-zero means this estimate cannot be sent as-is. |
| narrative | string or null | |
| flags | string[] | |
| expiresAt | string or null | |
| sentAt | string or null | |
| acceptedAt | string or null | |
| lines | EstimateLine[] | |
| createdAt | string | |
| updatedAt | string |
data → lines
| Field | Type | Notes |
|---|---|---|
| id | uuid | |
| lineNumber | integer | |
| description | string or null | |
| coverage | string or null | What this line covers, as it appears on the customer document. |
| quantity | number | |
| unitPriceCents | integer or null | |
| amountCents | integer or null | Null means UNPRICED (no rate on file). It is not zero, and treating it as zero publishes free work. |
| status | string | |
| catalogItemId | uuid or null | |
| siteId | uuid or null | |
| treeIds | uuid[] | Which captured trees this line prices. |
Example response 200
{
"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": "3a7c9e21-5b48-4f0a-9d33-71c6ee204b18",
"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": "3a7c9e21-5b48-4f0a-9d33-71c6ee204b18",
"treeIds": []
}
],
"createdAt": "2026-08-25T18:22:41.771Z",
"updatedAt": "2026-08-25T18:22:41.771Z"
}
}Failures
401Missing, malformed, revoked or expired credential. TheWWW-Authenticateheader names the scheme, which isApiKey.403The key is valid but does not carry the scope this operation requires. The message names the scope, so you can ask for exactly the grant you need.404No such record in your organization. A record belonging to somebody else is indistinguishable from one that does not exist.422Validation failed. Every bad field is named, at once.429Rate limit exceeded.500Something failed on our side. Retry with the SAME Idempotency-Key; a 5xx is the one status where retrying is correct.