API reference
Read a tree inventory, create customers and sites, and write an estimate. Built for programs first: one response shape, status codes that mean what they say, and an example on every call.
Base URL https://treeinventory.ai/api/v1 · OpenAPI 3.1 spec · llms.txt
Authentication
Send your key on every request. The scheme word is ApiKey, not Bearer. A bearer token here would be a session token, and this surface deliberately does not accept one.
curl -s "https://treeinventory.ai/api/v1/customers" \
-H "Authorization: ApiKey tiai_YOUR_KEY"A key belongs to one organization and carries a fixed set of scopes. There is no orgId field anywhere in this API, and a record belonging to another organization answers 404, exactly as one that does not exist.
One envelope, always
Success and failure both return {success, message, data}. A client that parses the success shape will not crash on the first failure it meets.
{ "success": true, "message": "Customers retrieved", "data": { "items": [], "nextCursor": null } }
{ "success": false, "message": "Invalid API key.", "data": { "code": "unauthorized" } }Branch on data.code, never on message. The codes are stable; the prose is not.
Status codes, and when to retry
- 401: missing, malformed, revoked or expired key. The
WWW-Authenticateheader names the scheme. - 403: the key is real but lacks the scope. The message names the scope you need.
- 404: no such record in your organization. Never a 500.
- 409: an idempotency conflict. See below.
- 422: validation. Every bad field is named at once, so one retry can fix all of them.
- 429: rate limited. Wait
Retry-Afterseconds. - 5xx: our fault. Retry with the same Idempotency-Key.
Retry a 5xx and a 429. Never retry a 4xx: it will fail identically forever, and a retry loop against a request that cannot succeed is the most common way an integration burns its rate limit.
Rate limits
1,000 requests per hour per key. Every response carries the limit, not just the 429, so you can pace yourself before you are refused.
RateLimit-Limit: 1000
RateLimit-Remaining: 993
RateLimit-Reset: 2841 # SECONDS remaining, not a timestamp
Retry-After: 2841 # on 429 only, same unitIdempotency
Mint a UUID per logical action and send it as Idempotency-Key on every write. Reuse it on every retry of that action.
- Same key, same body: you get the original response back with
Idempotent-Replay: true, and nothing is created. - Same key, different body: 409. We will not guess which one you meant.
- Same key, still in flight: 409 with
Retry-After. - A request that fails releases its key, so a validation error does not burn it.
- No key at all: a retry after a timeout creates a second record. That is on you.
Pagination
Lists are cursor-based, oldest first by creation. Read data.nextCursor and send it back as ?cursor= until it is null. The cursor is opaque: do not parse it.
There is no offset and no page number, deliberately. A record created while you are paging appears at the end, and nothing you have already seen moves, which is not true of offset pagination, where an insert silently skips a record for you.
From a spreadsheet to a quote
The four calls that turn a spreadsheet row into a draft estimate, with real request and response bodies you can copy and run.