Invoices
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.
https://app.treeinventory.ai/api/v1/invoicesList invoices
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.
Scope: invoices:read
Query parameters
| Field | Type | Notes |
|---|---|---|
| cursor | string | 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. |
| limit | integer | Page size. Defaults to 50, clamped to 200 rather than refused. |
| customerId | uuid | Only this customer's invoices. |
| state | string | Only invoices in this state. |
Each item in data.items
| Field | Type | Notes |
|---|---|---|
| id | uuid | |
| number | string or null | "INV-0051" once posted. Null while the invoice is still a draft. |
| state | string | draft | posted | voided. |
| financialStatus | "draft" | "open" | "partial" | "paid" | "overdue" | "voided" | 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. |
| customerId | uuid | Who owes the money. |
| estimateId | uuid or null | The estimate this was billed from, when it came from one. |
| estimateNumber | string or null | "JOB-0062". |
| currencyCode | string | |
| subtotalCents | integer | |
| discountCents | integer | |
| taxCents | integer | |
| totalCents | integer | What was billed. |
| paidCents | integer | What has been received against it. |
| balanceCents | integer | What is still owed. Read this one rather than subtracting: it is a projection that already accounts for partial payments and voids. |
| lineCount | integer | |
| transactionDate | string | The invoice date, YYYY-MM-DD. |
| dueDate | string | YYYY-MM-DD. |
| postedAt | string or null | |
| voidedAt | string or null | |
| voidedReason | string or null | |
| firstSentAt | string or null | When a human first sent it to the customer. Not settable here. |
| createdAt | string | |
| updatedAt | string |
Example response 200
{
"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
}
}Failures
401Missing, malformed, revoked or expired credential. TheWWW-Authenticateheader names the scheme, which isApiKey. Carries noRateLimit-*headers: the credential was never resolved, so there is no bucket to report.403The key is valid but does not carry the scope this operation requires. The message names the scope, so you can ask for exactly the grant you need. Carries noRateLimit-*headers: a request refused for scope is not counted against your limit.404No such record in your organization. A record belonging to somebody else is indistinguishable from one that does not exist.422Validation failed. Every bad field is named, at once.429Rate limit exceeded.500Something failed on our side. Retry with the SAME Idempotency-Key; a 5xx is the one status where retrying is correct.
https://app.treeinventory.ai/api/v1/invoicesBill a completed estimate
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.
The 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.
Scope: invoices: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 |
|---|---|---|
| estimateIdrequired | uuid | 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. |
| transactionDate | string | The invoice date. Defaults to today. |
| dueDate | string | Defaults to 30 days after the transaction date. Cannot be earlier than it. |
Example request
{
"estimateId": "e5d4c3b2-a190-4877-b6e5-d4c3b2a19087",
"dueDate": "2026-09-29"
}data
| Field | Type | Notes |
|---|---|---|
| id | uuid | |
| number | string or null | "INV-0051" once posted. Null while the invoice is still a draft. |
| state | string | draft | posted | voided. |
| financialStatus | "draft" | "open" | "partial" | "paid" | "overdue" | "voided" | 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. |
| customerId | uuid | Who owes the money. |
| estimateId | uuid or null | The estimate this was billed from, when it came from one. |
| estimateNumber | string or null | "JOB-0062". |
| currencyCode | string | |
| subtotalCents | integer | |
| discountCents | integer | |
| taxCents | integer | |
| totalCents | integer | What was billed. |
| paidCents | integer | What has been received against it. |
| balanceCents | integer | What is still owed. Read this one rather than subtracting: it is a projection that already accounts for partial payments and voids. |
| lineCount | integer | |
| transactionDate | string | The invoice date, YYYY-MM-DD. |
| dueDate | string | YYYY-MM-DD. |
| postedAt | string or null | |
| voidedAt | string or null | |
| voidedReason | string or null | |
| firstSentAt | string or null | When a human first sent it to the customer. Not settable here. |
| createdAt | string | |
| updatedAt | string |
Example response 201
{
"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"
}
}Failures
401Missing, malformed, revoked or expired credential. TheWWW-Authenticateheader names the scheme, which isApiKey. Carries noRateLimit-*headers: the credential was never resolved, so there is no bucket to report.403The key is valid but does not carry the scope this operation requires. The message names the scope, so you can ask for exactly the grant you need. Carries noRateLimit-*headers: a request refused for scope is not counted against your limit.404No such record in your organization. A record belonging to somebody else is indistinguishable from one that does not exist.409Idempotency conflict.idempotency_key_reusedmeans thisIdempotency-Keywas already used with a DIFFERENT request body: mint a new key for a new request, or resend the original body to replay.command_in_progressmeans an identical request is still in flight; waitRetry-Afterseconds and retry.422Validation failed. Every bad field is named, at once.429Rate limit exceeded.500Something failed on our side. Retry with the SAME Idempotency-Key; a 5xx is the one status where retrying is correct.
https://app.treeinventory.ai/api/v1/invoices/{invoiceId}Get one invoice
Whether it is paid, and what is left. paidCents and balanceCents come from a projection that already accounts for partial payments and voids.
Scope: invoices:read
Path parameters
| Field | Type | Notes |
|---|---|---|
| invoiceIdrequired | uuid | The invoice's id. |
data
| Field | Type | Notes |
|---|---|---|
| id | uuid | |
| number | string or null | "INV-0051" once posted. Null while the invoice is still a draft. |
| state | string | draft | posted | voided. |
| financialStatus | "draft" | "open" | "partial" | "paid" | "overdue" | "voided" | 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. |
| customerId | uuid | Who owes the money. |
| estimateId | uuid or null | The estimate this was billed from, when it came from one. |
| estimateNumber | string or null | "JOB-0062". |
| currencyCode | string | |
| subtotalCents | integer | |
| discountCents | integer | |
| taxCents | integer | |
| totalCents | integer | What was billed. |
| paidCents | integer | What has been received against it. |
| balanceCents | integer | What is still owed. Read this one rather than subtracting: it is a projection that already accounts for partial payments and voids. |
| lineCount | integer | |
| transactionDate | string | The invoice date, YYYY-MM-DD. |
| dueDate | string | YYYY-MM-DD. |
| postedAt | string or null | |
| voidedAt | string or null | |
| voidedReason | string or null | |
| firstSentAt | string or null | When a human first sent it to the customer. Not settable here. |
| createdAt | string | |
| updatedAt | string |
Example response 200
{
"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"
}
}Failures
401Missing, malformed, revoked or expired credential. TheWWW-Authenticateheader names the scheme, which isApiKey. Carries noRateLimit-*headers: the credential was never resolved, so there is no bucket to report.403The key is valid but does not carry the scope this operation requires. The message names the scope, so you can ask for exactly the grant you need. Carries noRateLimit-*headers: a request refused for scope is not counted against your limit.404No such record in your organization. A record belonging to somebody else is indistinguishable from one that does not exist.422Validation failed. Every bad field is named, at once.429Rate limit exceeded.500Something failed on our side. Retry with the SAME Idempotency-Key; a 5xx is the one status where retrying is correct.