Quotes
Create and manage quotes.
A quote is the priced proposal you send a customer before work begins — they can accept or decline it. This page covers the API tools that manage the quote shell: its title, attached customer, and status. Line items and sections are added through the Zoop quote editor, not via these tools.
The quote moves through a fixed lifecycle: draft → sent → accepted or declined. Archiving a quote is a separate, one-way action handled by quotes.archive — it hides the record without changing its status.
Scopes
| Scope | Grants |
|---|---|
read:quotes | quotes.list, quotes.get |
write:quotes | quotes.create, quotes.update, quotes.archive |
write:quotes does not bundle read:quotes. If your integration needs to both read and write quotes, request both scopes.
Legacy aliases. The older scopes read:estimates and write:estimates are aliases for read:quotes and write:quotes. Tokens issued before the rename continue to work. For new integrations, use read:quotes / write:quotes.
See scopes for the full scope catalog.
Who can write quotes
All write tools (quotes.create, quotes.update, quotes.archive) have two requirements on top of the write:quotes scope:
- User-bound token — the token must be tied to a real team member. Use a user-bound API key (prefix
zoop_uk_) or an OAuth access token. Tenant-key tokens (prefixzoop_tk_) represent the account itself, not a person, and cannot write quotes. - Owner role — the team member must hold the
ownerrole. Theofficeandtechroles cannot write quotes even if the token carrieswrite:quotes; the call returnsinvalid_input.
Read tools (quotes.list, quotes.get) accept any valid read:quotes token — user-bound or tenant-key, any role.
If you need to create or update quotes from an automated pipeline (no human in the loop), issue a user-bound API key to an owner-role service account. See API keys.
Tools
quotes.list
Returns quotes for your account, newest first. Archived quotes are excluded unless you specifically request them.
Scope: read:quotes
Filter to one lifecycle state. Accepted values: draft, sent, accepted, declined. When omitted, all non-archived quotes are returned.
1-based page number. Defaults to 1.
Returns an object with data (array of quotes), count (total matching), page, and limit.
quotes.get
Fetches a single quote by ID.
Scope: read:quotes
The quote's ID.
Returns the full quote record. Returns not_found if the quote ID does not exist in your account or the quote has been archived.
quotes.create
Creates a new quote in draft status.
Scope: write:quotes — requires a user actor with the owner role.
The quote title, min 1 character, max 500 characters. This appears as the document heading.
The customer to attach the quote to. Must belong to this tenant. Omit or pass null to create an unattached quote.
Returns the newly created quote record. Returns invalid_input if customer_id does not belong to your account.
Line items, sections, and attachments are not accepted here. After creating the quote, open it in the Zoop quote editor to add line items and sections.
quotes.update
Updates one or more fields on an existing quote. Fields you omit are left unchanged.
Scope: write:quotes — requires a user actor with the owner role.
The quote's ID.
Replacement title, min 1 character, max 500 characters.
Replace or clear the attached customer. Pass null to detach.
Advance the lifecycle. Allowed transitions:
| From | To |
|---|---|
draft | sent |
sent | accepted, declined |
accepted | (terminal) |
declined | (terminal) |
An illegal transition (for example, moving from accepted back to draft) returns a conflict error. To hide a quote without changing its status, use quotes.archive.
Returns the updated quote on success. Returns not_found if the quote does not exist. Returns conflict if the status transition is illegal.
quotes.archive
Archives a quote. The record is hidden from quotes.list and quotes.get results, but its history is preserved. Archiving is one-way — there is no unarchive tool.
Scope: write:quotes — requires a user actor with the owner role.
The quote's ID.
Returns { archived: true, id } on success. Returns not_found if the quote ID does not exist in your account. Returns conflict if the quote is already archived.
Status lifecycle
draft ──→ sent ──→ accepted
└──→ declined
accepted and declined are terminal — you cannot change the status after reaching either. Archiving is separate from the lifecycle: you can archive a quote at any status.
Example: create a quote and mark it sent
This example creates a draft quote for a customer and then advances it to sent. You will need a zoop_uk_ API key (owner role) and a valid customer_id from Customers.
Create the 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": 1,
"method": "tools/call",
"params": {
"name": "quotes.create",
"arguments": {
"title": "Bathroom remodel — 123 Elm St",
"customer_id": "cccccccc-cccc-cccc-cccc-cccccccccccc"
}
}
}'
A successful response returns the new quote. The status field shows where the quote sits in its lifecycle — draft right after creation:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "{"id":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb","tenant_id":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","customer_id":"cccccccc-cccc-cccc-cccc-cccccccccccc","title":"Bathroom remodel — 123 Elm St","status":"draft","accepted_at":null,"created_by":"uuuuuuuu-uuuu-uuuu-uuuu-uuuuuuuuuuuu","created_at":"2026-06-13T09:00:00Z","updated_at":"2026-06-13T09:00:00Z"}"
}
]
}
}
Advance to sent
After adding line items in the Zoop quote editor, mark the quote as sent. Use the id returned in step 1.
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": "quotes.update",
"arguments": {
"id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
"status": "sent"
}
}
}'
Example: list accepted quotes
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: 1,
method: 'tools/call',
params: {
name: 'quotes.list',
arguments: { status: 'accepted', page: 1 },
},
}),
})
const envelope = await response.json()
const result = JSON.parse(envelope.result?.content?.[0]?.text)
console.log(`${result.count} accepted quote(s)`)
for (const quote of result.data) {
console.log(quote.id, quote.title)
}
Error reference
| Error kind | When it occurs |
|---|---|
not_found | The quote ID does not exist in this tenant or the quote is already archived. |
invalid_input | A required field is missing, input failed validation, the token is a tenant-key, or the caller does not hold the owner role. |
conflict | The requested status transition is illegal, or the quote is already archived. |
insufficient_scope | The token does not hold the required read:quotes or write:quotes scope. |
internal | An unexpected server error — details are not returned to the client. |
See errors for the full error reference and HTTP status codes.
Related
- Quotes in Zoop — end-user guide for creating and sending quotes
- Invoices — turn an accepted quote into an invoice
- Customers — look up a
customer_idto attach to a quote - Scopes — full scope catalog
- MCP server — transport, conventions, and the full tool catalog
- Errors — error kinds and HTTP status codes