API referenceNotes

Notes

Create and manage notes attached to other records.

Notes are free-text records you can attach to almost any record in Zoop — a customer, contact, location, job, invoice, quote, or estimate — or leave standalone. They support pinning, full-text search, date filtering, and archiving (hiding without deleting).

Notes use their own permission scopes (read:notes / write:notes), separate from the scopes for the records they attach to. See scopes for background on how scopes work.

Scopes

ScopeGrants
read:notesnotes.list, notes.get
write:notesnotes.create, notes.update, notes.archive

write:notes does not automatically include read:notes. If your integration needs to both read and write notes, request both scopes.

Write operations require a user identity

notes.create, notes.update, and notes.archive require a user actor — meaning the API key must be tied to a real team member so Zoop knows who authored the note. Tenant-level keys (the zoop_tk_ prefix) represent the account as a whole, not a person, so they cannot create or modify notes. If you try, you'll get an invalid_input error.

If you need to write notes from an automated pipeline with no human in the loop, create a dedicated service-account team member in Zoop and issue a user-bound API key (zoop_uk_) for that account. See API keys.

Tools

Each tool below is called by sending a JSON-RPC request to the Zoop MCP endpoint (POST /api/mcp). See the MCP server page for the full request envelope and transport details.

notes.list

Returns a list of notes for your tenant, sorted newest first. Results are paged — you get up to 25 notes per call (max 100), and a cursor you can use to fetch the next page.

Scope: read:notes

path
parameter

When true, returns only pinned notes.

path
parameter

Filter by the type of record the note is attached to. Accepted values: customer, customer_contact, customer_location, job, invoice, quote, lawn_estimate, standalone. Pass standalone to return only notes with no subject attached.

path
parameter

Filter by a specific subject record's ID. Combine with subject_type to scope to a single record. Cannot be used when subject_type is standalone.

path
parameter

Full-text search query, max 256 characters. Cannot be combined with subject_type: "standalone".

path
parameter

Return notes created at or after this timestamp. Example: 2026-01-01T00:00:00Z.

path
parameter

Return notes created before this timestamp (exclusive).

path
parameter

Filter by author user ID.

path
parameter

Opaque cursor returned as nextCursor in the previous response. Pass it to fetch the next page.

path
parameter

Number of rows to return. Default 25, max 100.

Returns: { rows, nextCursor, hasMore }

Each row includes the note's id, body, pinned, created_by, created_at, subject_type, subject_id, and any attached file URLs. Draft notes are excluded from results.


notes.get

Returns a single note by its ID.

Scope: read:notes

path
parameter

The note's ID.

Returns the full note record. Returns not_found if the note does not exist or has been archived.


notes.create

Creates a note. Optionally attaches it to another record.

Scope: write:notes — requires a user actor.

path
parameter

The note text, max 100,000 characters. Must be non-empty unless draft is true.

path
parameter

The type of record to attach the note to. Accepted values: customer, customer_contact, customer_location, job, invoice, quote, lawn_estimate. Omit to create a standalone note.

path
parameter

The ID of the record to attach the note to. subject_type and subject_id must be provided together or both omitted.

path
parameter

Pin the note. Defaults to false.

path
parameter

Mark the note as a draft. Drafts are hidden from default list results and are cleaned up automatically after 24 hours if not committed. Defaults to false.

Returns the created note record on success. Returns not_found if the subject_id does not exist.


notes.update

Partially updates a note. Only the fields you send are changed.

Scope: write:notes — requires a user actor.

Only the note's author or a tenant owner can update a note.

path
parameter

The note's ID.

path
parameter

Replacement text, min 1 character, max 100,000 characters.

path
parameter

Set or clear the pin.

path
parameter

Change or clear the attached subject type. Send both subject_type and subject_id together. Pass both as null to detach the note from its current subject.

path
parameter

Change or clear the attached subject ID. Must be provided with subject_type.

path
parameter

Commit a draft note by setting draft: false. You cannot re-draft a committed note.

At least one field is required. Returns not_found if the note does not exist or is already archived. Returns invalid_input if the caller is not the author and does not hold the owner role.


notes.archive

Archives a note. The note is hidden from notes.list and notes.get results, but its data is preserved in Zoop's database (it is not deleted).

Scope: write:notes — requires a user actor.

Only the note's author or a tenant owner can archive a note.

path
parameter

The note's ID.

Returns { archived: true, id } on success. Returns not_found if the note does not exist or is already archived.


Example: list pinned notes on a job

The request below fetches the first page of pinned notes attached to a specific job. Replace zoop_uk_<your-secret> with your API key and the subject_id value with the real job 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": "notes.list",
      "arguments": {
        "subject_type": "job",
        "subject_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "pinned": true,
        "limit": 25
      }
    }
  }'

A successful response looks like:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{"rows":[{"id":"dddddddd-dddd-dddd-dddd-dddddddddddd","body":"Ran a new 20A circuit from the panel. Inspector visit scheduled for Friday.","pinned":true,"created_by":"uuuuuuuu-uuuu-uuuu-uuuu-uuuuuuuuuuuu","created_at":"2026-06-12T14:30:00Z","subject_type":"job","subject_id":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","attachments":[]}],"nextCursor":null,"hasMore":false}"
      }
    ]
  }
}

Error reference

Error kindMeaning
not_foundThe note ID does not exist in this tenant, the note is already archived, or the subject record (subject_id) was not found.
invalid_inputA required field is missing, the input failed validation, the caller is a tenant actor, or the caller is not the author/owner.
insufficient_scopeThe token does not hold read:notes or write:notes as required.
conflictThe write rate limit for this user was exceeded. Slow down your requests and retry.
internalAn unexpected server error — details are not returned to the client.

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

Pagination

notes.list uses cursor-based pagination. Instead of page numbers, each response includes a nextCursor token. When hasMore is true, pass that token as the cursor parameter on your next call to get the following page.

let cursor: string | undefined
let allNotes: unknown[] = []

do {
  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: 'notes.list',
        arguments: { subject_type: 'job', subject_id: JOB_ID, limit: 100, cursor },
      },
    }),
  })

  // Parse the MCP text content to extract the tool result
  const envelope = await response.json()
  const text = envelope.result?.content?.[0]?.text
  const page = JSON.parse(text)

  allNotes = allNotes.concat(page.rows)
  cursor = page.nextCursor ?? undefined
} while (cursor)

If nextCursor is null but hasMore is still true, the result set was truncated — this can happen with full-text search queries. You cannot page past this point. Narrow your query with additional filters (for example, a tighter date range or a more specific search term) to get a complete result set.

  • Notes in Zoop — end-user guide for working with notes in the product
  • 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