API referenceQuotes

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: draftsentaccepted or declined. Archiving a quote is a separate, one-way action handled by quotes.archive — it hides the record without changing its status.

Scopes

ScopeGrants
read:quotesquotes.list, quotes.get
write:quotesquotes.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:

  1. 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 (prefix zoop_tk_) represent the account itself, not a person, and cannot write quotes.
  2. Owner role — the team member must hold the owner role. The office and tech roles cannot write quotes even if the token carries write:quotes; the call returns invalid_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

path
parameter

Filter to one lifecycle state. Accepted values: draft, sent, accepted, declined. When omitted, all non-archived quotes are returned.

path
parameter

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

path
parameter

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.

path
parameter

The quote title, min 1 character, max 500 characters. This appears as the document heading.

path
parameter

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.

path
parameter

The quote's ID.

path
parameter

Replacement title, min 1 character, max 500 characters.

path
parameter

Replace or clear the attached customer. Pass null to detach.

path
parameter

Advance the lifecycle. Allowed transitions:

FromTo
draftsent
sentaccepted, 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.

path
parameter

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 kindWhen it occurs
not_foundThe quote ID does not exist in this tenant or the quote is already archived.
invalid_inputA required field is missing, input failed validation, the token is a tenant-key, or the caller does not hold the owner role.
conflictThe requested status transition is illegal, or the quote is already archived.
insufficient_scopeThe token does not hold the required read:quotes or write:quotes scope.
internalAn unexpected server error — details are not returned to the client.

See errors for the full error reference and HTTP status codes.

  • Quotes in Zoop — end-user guide for creating and sending quotes
  • Invoices — turn an accepted quote into an invoice
  • Customers — look up a customer_id to attach to a quote
  • Scopes — full scope catalog
  • MCP server — transport, conventions, and the full tool catalog
  • Errors — error kinds and HTTP status codes