API referenceCatalog items

Catalog items

Manage pricebook items — services, products, labor, fees, bundles, and discounts — via the Zoop MCP tool layer.

This page documents the five MCP tools that manage catalog items (your pricebook) in Zoop. You will use these tools to create, read, update, and archive the line-item definitions that get pulled into quotes, jobs, and invoices.

Each item has a kind that controls which fields apply. The six kinds are:

KindWhat it is
serviceA billable service (e.g. drain cleaning, HVAC tune-up).
productA physical product sold to the customer (e.g. filter, part).
laborA labor rate charged by the hour or by the task.
feeA flat surcharge (e.g. travel fee, disposal fee).
bundleA package that contains other catalog items.
discountA percentage or flat-amount discount applied to a line.

All tools in this group require the read:catalog_items or write:catalog_items scope. Scopes are permissions that control what an API token can do. The broader read:catalog and write:catalog umbrella scopes automatically include these — see Scopes.

If you are new to the MCP layer, read MCP overview first to learn how to connect and authenticate.


Scopes

ScopeGrants access to
read:catalog_itemscatalog_items.list, catalog_items.get
write:catalog_itemscatalog_items.create, catalog_items.update, catalog_items.archive

write:catalog_items does not include read:catalog_items. Request both scopes if your integration needs to read and write.

The umbrella scopes read:catalog and write:catalog expand to the item and category child scopes at verification time — you can use either the umbrella or the specific child scope.


Owner-only fields

Some fields are only visible when the API token belongs to a user with the owner role. If you are using a token for an office or tech user, or a tenant-key token (an API key that is not tied to any specific user), you get the same response shape but those fields are omitted:

FieldApplies to kindsWhat it tracks
costservice, product, labor, feeYour cost for the item (not the customer price).
markup_pctservice, product, labor, fee, bundleMarkup percentage over cost.
supplier_urlservice, product, labor, fee, bundleURL to the supplier or part listing.
supplier_skuservice, product, labor, fee, bundleSupplier's part number or SKU.
last_known_costservice, product, labor, feeMost recent cost from an external price sync.
last_synced_atservice, product, labor, feeTimestamp of the last cost sync.

discount items have no owner-only fields — the response shape is the same for every role.

A tenant-key token always receives the non-owner (public) shape, regardless of which scopes it carries.


catalog_items.list

List catalog items for the tenant. Results are newest first. Each row includes the item's id and summary fields. To get bundle components or the full record, call catalog_items.get.

body
kind

Filter by item kind. One of service, product, labor, fee, bundle, or discount. Omit to return all kinds.

body
category_id

Filter to items in this category. Pass the category id.

body
active

When true, return only non-archived items. When false, return only archived items. Omit to return all.

body
limit

Maximum number of items to return. Integer between 1 and 200.

Example

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "catalog_items.list",
    "arguments": {
      "kind": "service",
      "active": true,
      "limit": 50
    }
  }
}
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "[{"id":"e1f2a3b4-0000-0000-0000-000000000001","kind":"service","name":"Drain cleaning","description":"Snake the main drain line","sku":"SVC-001","unit":"job","unit_price":185.00,"category_id":null,"image_url":null,"metadata":{},"created_at":"2026-01-10T09:00:00.000Z","updated_at":"2026-01-10T09:00:00.000Z","archived_at":null}]"
      }
    ]
  }
}

catalog_items.get

Fetch one catalog item by id. Bundle items include a components array listing the items in the bundle. If the item has been archived or the id does not exist, the call returns a not_found error.

body
id

The catalog item's id.

Example — fetching a bundle

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "catalog_items.get",
    "arguments": {
      "id": "b9c8d7e6-0000-0000-0000-000000000002"
    }
  }
}
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{"id":"b9c8d7e6-0000-0000-0000-000000000002","kind":"bundle","name":"HVAC tune-up package","description":null,"sku":"BDL-HVAC-01","unit":null,"unit_price":null,"flat_package":false,"category_id":null,"image_url":null,"metadata":{},"components":[{"id":"cc000001-0000-0000-0000-000000000001","bundle_id":"b9c8d7e6-0000-0000-0000-000000000002","catalog_item_id":"e1f2a3b4-0000-0000-0000-000000000001","default_qty":1,"sort_order":0,"created_at":"2026-01-15T10:00:00.000Z"}],"created_at":"2026-01-15T10:00:00.000Z","updated_at":"2026-01-15T10:00:00.000Z","archived_at":null}"
      }
    ]
  }
}

catalog_items.create

Create a new catalog item. The kind field is required and determines which other fields are allowed or required.

catalog_items.create requires a token that identifies a specific user — either a user-bound API key or an OAuth token. A tenant-key API key (one that is not tied to a user) cannot call this tool.

body
kind

The item variant. One of service, product, labor, fee, bundle, or discount. You cannot change kind after creation.

body
name

Display name for the item. Max 255 characters.

body
description

Longer description shown on quotes and invoices. Max 2,000 characters.

body
sku

Your internal SKU or item code. Max 128 characters.

body
unit

Unit label shown on line items (e.g. hr, job, each). Not applicable to discount items. Max 64 characters.

body
unit_price

The price you charge the customer per unit. Non-negative. Not applicable to discount items.

body
category_id

Assign the item to a category. The category must belong to your tenant. See Catalog categories.

body
cost

Your cost for the item (owner-only field). Non-negative. Not applicable to bundle or discount items.

body
markup_pct

Markup percentage over cost (owner-only field). Non-negative. Not applicable to discount items.

body
supplier_url

URL to the supplier or part listing (owner-only field). Must be a valid URL. Not applicable to discount items.

body
supplier_sku

Supplier's part number (owner-only field). Max 128 characters. Not applicable to discount items.

body
discount_type

Required when kind is discount. One of percentage or flat.

body
discount_value

The discount amount. For percentage, this is a percentage (e.g. 10 for 10%). For flat, this is a currency amount. Non-negative. Only applicable to discount items.

body
flat_package

When true, the bundle is priced as a flat package rather than summing component prices. Only applicable to bundle items.

body
components

The items in the bundle. Required when kind is bundle; must contain at least one component.

body
metadata

Arbitrary key-value data you want to store alongside the item. Keys and values must be JSON-serializable.

Example — create a service item

{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "catalog_items.create",
    "arguments": {
      "kind": "service",
      "name": "Drain cleaning",
      "description": "Snake the main drain line",
      "sku": "SVC-001",
      "unit": "job",
      "unit_price": 185.00
    }
  }
}

Example — create a discount item

{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "tools/call",
  "params": {
    "name": "catalog_items.create",
    "arguments": {
      "kind": "discount",
      "name": "Senior discount",
      "discount_type": "percentage",
      "discount_value": 10
    }
  }
}

Example — create a bundle

{
  "jsonrpc": "2.0",
  "id": 5,
  "method": "tools/call",
  "params": {
    "name": "catalog_items.create",
    "arguments": {
      "kind": "bundle",
      "name": "HVAC tune-up package",
      "sku": "BDL-HVAC-01",
      "components": [
        {
          "catalog_item_id": "e1f2a3b4-0000-0000-0000-000000000001",
          "default_qty": 1,
          "sort_order": 0
        },
        {
          "catalog_item_id": "a2b3c4d5-0000-0000-0000-000000000002",
          "default_qty": 2,
          "sort_order": 1
        }
      ]
    }
  }
}

catalog_items.update

Update a catalog item's mutable fields. This is a partial update — only the fields you send are changed. Fields you omit are left as-is.

For bundle items, passing components replaces the entire component list in one operation. If you want to add one component to an existing bundle, fetch the current list first, add your new item, then send the full updated list. Omitting components leaves the existing list unchanged.

catalog_items.update requires a token that identifies a specific user. A tenant-key API key cannot call this tool.

body
id

The catalog item to update.

body
name

Updated display name. Max 255 characters.

body
description

Updated description. Max 2,000 characters. Pass null to clear.

body
sku

Updated SKU. Max 128 characters. Pass null to clear.

body
unit

Updated unit label. Max 64 characters. Pass null to clear.

body
unit_price

Updated customer price. Non-negative. Pass null to clear.

body
category_id

Assign or reassign the item to a category. The category must belong to your tenant. Pass null to clear the category.

body
cost

Updated cost (owner-only). Non-negative. Pass null to clear.

body
markup_pct

Updated markup percentage (owner-only). Non-negative. Pass null to clear.

body
supplier_url

Updated supplier URL (owner-only). Must be a valid URL. Pass null to clear.

body
supplier_sku

Updated supplier SKU (owner-only). Max 128 characters. Pass null to clear.

body
discount_type

Updated discount type for discount items. One of percentage or flat. Required when updating discount_value.

body
discount_value

Updated discount amount for discount items. Non-negative. Pass null to clear. When providing discount_value, you must also provide discount_type.

body
flat_package

Toggle flat-package pricing on a bundle item. Pass null to clear.

body
components

Replacement component list for a bundle item. When provided, this list replaces the entire existing list. Must follow the same shape as in catalog_items.create. Omit to leave the current components unchanged.

body
metadata

Replacement metadata object. Replaces the entire metadata map when provided.

Example — update a price

{
  "jsonrpc": "2.0",
  "id": 6,
  "method": "tools/call",
  "params": {
    "name": "catalog_items.update",
    "arguments": {
      "id": "e1f2a3b4-0000-0000-0000-000000000001",
      "unit_price": 195.00
    }
  }
}

Example — replace bundle components

{
  "jsonrpc": "2.0",
  "id": 7,
  "method": "tools/call",
  "params": {
    "name": "catalog_items.update",
    "arguments": {
      "id": "b9c8d7e6-0000-0000-0000-000000000002",
      "components": [
        {
          "catalog_item_id": "e1f2a3b4-0000-0000-0000-000000000001",
          "default_qty": 1,
          "sort_order": 0
        },
        {
          "catalog_item_id": "a2b3c4d5-0000-0000-0000-000000000002",
          "default_qty": 1,
          "sort_order": 1
        },
        {
          "catalog_item_id": "f3e4d5c6-0000-0000-0000-000000000003",
          "default_qty": 3,
          "sort_order": 2
        }
      ]
    }
  }
}

catalog_items.archive

Archive a catalog item. Archiving hides the item from default list results and from catalog_items.get, but the record and all history (quotes, invoices, jobs that already used it) is preserved. This is the only way to retire an item — there is no permanent delete.

You cannot archive a catalog item that is still a component in an active bundle. Remove it from all bundles first, then archive it.

catalog_items.archive requires a token that identifies a specific user. A tenant-key API key cannot call this tool.

body
id

The catalog item to archive.

Example

{
  "jsonrpc": "2.0",
  "id": 8,
  "method": "tools/call",
  "params": {
    "name": "catalog_items.archive",
    "arguments": {
      "id": "e1f2a3b4-0000-0000-0000-000000000001"
    }
  }
}
{
  "jsonrpc": "2.0",
  "id": 8,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{"archived":true,"id":"e1f2a3b4-0000-0000-0000-000000000001"}"
      }
    ]
  }
}

Error reference

Error typeWhat it meansCommon cause
invalid_inputA field failed validation, or a required actor condition was not metMissing kind, missing discount_type on a discount item, missing components on a bundle, calling a write tool with a tenant-key token
not_foundThe referenced item does not exist in this tenant or is already archivedStale or wrong id
conflictThe request collides with existing stateArchiving an item that is still a component in an active bundle
insufficient_scopeThe token does not hold the required scopeMissing read:catalog_items or write:catalog_items

For the full error shape and HTTP status codes, see Errors.