Invoices
Create and manage invoices and their line items.
Use these tools to create, update, and manage invoices — the billable documents you send to customers asking for payment.
In Zoop, every invoice starts as a draft and moves forward through a fixed set of statuses: draft → sent → paid (or overdue). When an invoice is no longer needed, you void it — voiding hides it from the default list but keeps the billing history intact. You cannot un-void an invoice.
Line items (the individual charges on an invoice) are covered by the same scope as the invoice itself — there is no separate read:invoice_line_items scope. See scopes.
Scopes
| Scope | Grants |
|---|---|
read:invoices | invoices.list, invoices.get |
write:invoices | invoices.create, invoices.update, invoices.void |
write:invoices does not include read:invoices. Grant both if your integration needs to read and write.
User actor requirement
invoices.create and invoices.update require a user actor — meaning the token must be tied to a real Zoop team member. Zoop records that team member's ID on every invoice for attribution.
There are two types of API keys:
- Tenant key (
zoop_tk_prefix) — represents the account as a whole, with no individual user identity attached. Callinginvoices.createorinvoices.updatewith a tenant key returns aninvalid_inputerror. - User key (
zoop_uk_prefix) — bound to a specific team member. Use this type for creating or updating invoices. You can issue azoop_uk_key to a dedicated service account team member for automated integrations.
See API keys for how to issue each type.
invoices.void does not require a user actor and works with either token type.
Invoice lifecycle
An invoice can only be in one status at a time, and it can only move forward — not backward. The table below shows what each status means and which transitions are allowed.
| Status | Meaning |
|---|---|
draft | Editable. All fields and line items can be changed. Not yet visible to the customer. |
sent | Sent to the customer. Non-status fields are locked. |
paid | Payment recorded. |
overdue | Past the due date without payment. |
void | Soft-deleted. Hidden from the default list but billing history is preserved. One-way — cannot be un-voided. |
You can move a non-voided invoice to a new status at any time using invoices.update. However, editing any other field — notes, dates, line items, customer — is only allowed while the invoice is still in draft status. Trying to change those fields on a sent or paid invoice returns a conflict error.
uncollectible, dunning, partially_refunded, and refunded are Stripe-managed statuses set by payment sync triggers. You cannot write these values via the API.
Tools
invoices.list
Returns a list of your invoices, newest first. Voided invoices are excluded from the default results — pass status: "void" to see them explicitly. Results are paginated at 20 rows per page.
Scope: read:invoices
Filter to a single lifecycle status. Accepted values: draft, sent, paid, overdue, void. When omitted, voided invoices are excluded.
1-based page number. Defaults to 1.
Returns: { data, count, page, limit }
Each item in data is a full invoice row. count is the total number of matching rows across all pages. limit is 20.
invoices.get
Fetches a single invoice by its ID, including all fields and computed totals (subtotal, tax, total).
Scope: read:invoices
The invoice's ID.
Returns the full invoice record. Returns not_found if the invoice does not exist in this tenant.
The response includes invoice header fields and computed totals, but not the individual line items. To read line items, call the HTTP REST endpoint with the invoice id instead.
invoices.create
Creates a new invoice in draft status. You supply the customer and line items; Zoop computes the subtotal, tax, and total for you — do not calculate or pass those values yourself.
Scope: write:invoices — requires a user actor (a zoop_uk_ key).
Creating an invoice from a quote or job ID is not supported via this tool. Supply customer_id and line_items directly. Use the HTTP REST route if you need to derive an invoice from an existing quote or job.
The customer this invoice belongs to. Must belong to your tenant.
The line items on the invoice. When supplied, totals are computed from these rows. When omitted, the invoice is created with a total of 0. Each item has:
Line item label, 1–200 characters.
Must be positive.
Price per unit, non-negative, max two decimal places. Example: 125.00.
Whether this line item is subject to tax.
The tax rate to apply to this line item, or null to use the invoice-level default. Must belong to your tenant.
A fallback tax rate applied to taxable line items that have tax_rate_id: null. Must belong to your tenant.
The date shown as the invoice issue date. Example: 2026-06-13T00:00:00Z.
The payment due date.
Internal or customer-facing notes, max 2,000 characters.
Returns the created invoice record. The status is "draft" and the total, subtotal, and tax_amount fields contain the server-computed values.
invoices.update
Updates an invoice. Only the fields you include in the request are changed — you do not need to resend the entire record.
Scope: write:invoices — requires a user actor (a zoop_uk_ key).
You can change the status on any non-voided invoice. All other fields — notes, issued_at, due_at, customer_id, default_tax_rate_id, and line_items — can only be changed while the invoice is in draft status. Sending those fields on a non-draft invoice returns a conflict error.
When you include line_items, the existing line items are replaced in full and totals are recomputed. Send line_items: [] to remove all line items.
The invoice's ID.
Move the invoice to a new lifecycle status. Accepted values: draft, sent, paid, overdue, void. Transitioning to void is equivalent to calling invoices.void.
Reassign the invoice to a different customer. Draft only. Must belong to your tenant.
Replace all line items wholesale. Totals are recomputed after replacement. Draft only. Uses the same shape as invoices.create. Sending an empty array removes all line items.
Change the invoice-level fallback tax rate. Draft only. Must belong to your tenant. When omitted from an update that includes line_items, the existing default rate is preserved.
Change the issue date. Draft only.
Change the due date. Draft only.
Change the notes, max 2,000 characters. Draft only.
Returns the updated invoice record on success. Returns not_found if the invoice does not exist. Returns conflict if you attempt to edit a non-status field on a non-draft invoice.
invoices.void
Voids an invoice. The invoice is hidden from the default list, but the billing history record is preserved. This action is permanent — a voided invoice cannot be un-voided.
Scope: write:invoices
The invoice's ID.
Returns { voided: true, id } on success. Returns not_found if the invoice does not exist in this tenant.
Example: create and send an invoice
The steps below walk through the full create-and-send flow for a single visit invoice. Replace zoop_uk_<your-secret> with your own user key, and replace the UUID placeholders with real IDs from your tenant.
Create the draft
Call invoices.create with your customer_id and line items. Zoop computes the totals and returns the new invoice's id.
curl -X POST https://app.zoop.example/api/mcp \
-H "Authorization: Bearer zoop_uk_<your-secret>" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "invoices.create",
"arguments": {
"customer_id": "cccccccc-cccc-cccc-cccc-cccccccccccc",
"issued_at": "2026-06-13T00:00:00Z",
"due_at": "2026-07-13T00:00:00Z",
"notes": "Thank you for the work order.",
"line_items": [
{
"description": "HVAC tune-up — 2-ton split system",
"quantity": 1,
"unit_price": 185.00,
"is_taxable": true,
"tax_rate_id": "tttttttt-tttt-tttt-tttt-tttttttttttt"
},
{
"description": "Refrigerant top-off (1 lb R-410A)",
"quantity": 1,
"unit_price": 45.00,
"is_taxable": true,
"tax_rate_id": null
}
],
"default_tax_rate_id": "tttttttt-tttt-tttt-tttt-tttttttttttt"
}
}
}'
The response body contains the new invoice's id along with the server-computed subtotal, tax_amount, and total. Copy the id — you need it in the next step.
Mark it as sent
Call invoices.update with status: "sent". Swap in the id from step 1. This status-only change does not require the invoice to be a draft.
curl -X POST https://app.zoop.example/api/mcp \
-H "Authorization: Bearer zoop_uk_<your-secret>" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "invoices.update",
"arguments": {
"id": "iiiiiiii-iiii-iiii-iiii-iiiiiiiiiiii",
"status": "sent"
}
}
}'
A successful create response looks like:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "{"id":"iiiiiiii-iiii-iiii-iiii-iiiiiiiiiiii","tenant_id":"tttttttt-tttt-tttt-tttt-tttttttttttt","customer_id":"cccccccc-cccc-cccc-cccc-cccccccccccc","invoice_number":"INV-0001","status":"draft","subtotal":230.00,"tax_amount":19.00,"total":249.00,"issued_at":"2026-06-13T00:00:00Z","due_at":"2026-07-13T00:00:00Z","notes":"Thank you for the work order.","created_at":"2026-06-13T10:00:00Z"}"
}
]
}
}
Example: list open invoices
The request below fetches the first page of sent invoices — the ones that have been sent to customers but not yet paid.
curl -X POST https://app.zoop.example/api/mcp \
-H "Authorization: Bearer zoop_uk_<your-secret>" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "invoices.list",
"arguments": {
"status": "sent",
"page": 1
}
}
}'
Pagination
invoices.list returns up to 20 invoices per request (one page). If your tenant has more than 20 matching invoices, you need to make multiple requests and stitch the results together. The response includes a count field — the total number of matching invoices across all pages — which lets you calculate how many pages exist. Increment page by one on each request until you have fetched them all.
The TypeScript example below shows a simple loop:
let page = 1
const limit = 20
let allInvoices: unknown[] = []
while (true) {
const response = await fetch('https://app.zoop.example/api/mcp', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.ZOOP_API_KEY}`,
'Content-Type': 'application/json',
'Accept': 'application/json, text/event-stream',
},
body: JSON.stringify({
jsonrpc: '2.0',
id: page,
method: 'tools/call',
params: {
name: 'invoices.list',
arguments: { status: 'sent', page },
},
}),
})
const envelope = await response.json()
const text = envelope.result?.content?.[0]?.text
const result = JSON.parse(text)
allInvoices = allInvoices.concat(result.data)
const totalPages = Math.ceil(result.count / limit)
if (page >= totalPages) break
page++
}
Error reference
Every error response includes a kind field. The table below covers the errors specific to invoice tools.
| Error kind | When it occurs |
|---|---|
not_found | The invoice ID does not exist in this tenant. |
conflict | You tried to edit a non-status field on a non-draft invoice, or the invoice number was already taken at creation time. |
invalid_input | A required field is missing, a UUID is malformed, unit_price has more than two decimal places, a referenced customer_id or tax_rate_id does not belong to your tenant, or the caller is a tenant-key token calling a user-actor-only tool. |
insufficient_scope | The token does not hold read:invoices or write:invoices as required. |
internal | An unexpected server error — details are not returned to the client. |
See errors for the full error reference and HTTP status codes.
Related
- Invoices in Zoop — end-user guide for working with invoices in the product
- Tax rates — manage the tax rates you can reference on line items
- Quotes — the pre-invoice estimating step
- Jobs — jobs that invoices can be linked to
- Scopes — full scope catalog
- Authentication — how to authenticate requests
- MCP server — transport, conventions, and the full tool catalog
- Errors — error kinds and HTTP status codes