From a spreadsheet to a quote
You have a row from a customer's spreadsheet: *Delap Road Properties, 3760 W Delap Rd, crown clean two front oaks at $450 each, deadwood the rear maple at $325.* Four calls turn that into a draft estimate the arborist can review and send.
Mint one UUID before you start and use it as the `Idempotency-Key` on every write below. If the connection drops halfway, run the whole thing again with the same ids: nothing will be created twice.
1. Find the customer
You have a name, not an id. Search for it before creating anything, or you will create a duplicate of a customer who already exists.
curl -s "https://treeinventory.ai/api/v1/customers?search=Delap" \
-H "Authorization: ApiKey tiai_YOUR_KEY"Response
{
"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": null
}
}No match? `POST /customers` with the name, then carry on. That call takes an `Idempotency-Key` too.
2. Find the site
An estimate prices work at a place. Filter the customer's sites and match the street line you were given.
curl -s "https://treeinventory.ai/api/v1/sites?customerId=9f1b2c3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d" \
-H "Authorization: ApiKey tiai_YOUR_KEY"Response
{
"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
}
}No match? `POST /sites` with the street, city, state and zip. Leave latitude and longitude out and we geocode it.
3. Read the catalog
Every estimate line references a catalog item id. This is the step people skip, and it is why their first POST /estimates returns a 404 they cannot explain.
curl -s "https://treeinventory.ai/api/v1/catalog/items" \
-H "Authorization: ApiKey tiai_YOUR_KEY"Response
{
"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",
"createdAt": "2026-06-02T09:12:00.000Z",
"updatedAt": "2026-06-02T09:12:00.000Z"
}
],
"nextCursor": null
}
}The catalog price is the org's default. You are free to send a different `unitPriceCents` on the line: the spreadsheet you were handed is the source of truth for this quote.
4. Write the estimate
One call, the whole quote. Prices are in whole cents. Do not send a total. It is computed here from your quantities and unit prices, and a total you send would be ignored.
curl -s -X POST "https://treeinventory.ai/api/v1/estimates" \
-H "Authorization: ApiKey tiai_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 4d0f2b16-9c3a-4e51-a8b7-2f6d1c0e3a94" \
-d @quote.jsonquote.json
{
"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
}
]
}Response
{
"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,
"lines": [
{ "lineNumber": 1, "description": "Crown clean, front oaks", "quantity": 2, "unitPriceCents": 45000, "amountCents": 90000 },
{ "lineNumber": 2, "description": "Deadwood, rear maple", "quantity": 1, "unitPriceCents": 32500, "amountCents": 32500 }
],
"createdAt": "2026-08-25T18:22:41.771Z",
"updatedAt": "2026-08-25T18:22:41.771Z"
}
}Send that same request again with the same `Idempotency-Key` and you get the same estimate back with an `Idempotent-Replay: true` header. Nothing is created. Change the body and keep the key, and you get a 409 instead of a silently wrong answer.
The estimate lands as a **draft**. It is not sent, and no customer sees it until the arborist reviews it in the app and sends it. That is deliberate: an API that could mail a price to somebody's customer without a human looking at it is not a feature anyone asked for.
Read it back with `GET /api/v1/estimates/{id}` whenever you want to check what landed.