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:
| Kind | What it is |
|---|---|
service | A billable service (e.g. drain cleaning, HVAC tune-up). |
product | A physical product sold to the customer (e.g. filter, part). |
labor | A labor rate charged by the hour or by the task. |
fee | A flat surcharge (e.g. travel fee, disposal fee). |
bundle | A package that contains other catalog items. |
discount | A 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
| Scope | Grants access to |
|---|---|
read:catalog_items | catalog_items.list, catalog_items.get |
write:catalog_items | catalog_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:
| Field | Applies to kinds | What it tracks |
|---|---|---|
cost | service, product, labor, fee | Your cost for the item (not the customer price). |
markup_pct | service, product, labor, fee, bundle | Markup percentage over cost. |
supplier_url | service, product, labor, fee, bundle | URL to the supplier or part listing. |
supplier_sku | service, product, labor, fee, bundle | Supplier's part number or SKU. |
last_known_cost | service, product, labor, fee | Most recent cost from an external price sync. |
last_synced_at | service, product, labor, fee | Timestamp 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.
Filter by item kind. One of service, product, labor, fee, bundle, or discount. Omit to return all kinds.
Filter to items in this category. Pass the category id.
When true, return only non-archived items. When false, return only archived items. Omit to return all.
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.
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.
The item variant. One of service, product, labor, fee, bundle, or discount. You cannot change kind after creation.
Display name for the item. Max 255 characters.
Longer description shown on quotes and invoices. Max 2,000 characters.
Your internal SKU or item code. Max 128 characters.
Unit label shown on line items (e.g. hr, job, each). Not applicable to discount items. Max 64 characters.
The price you charge the customer per unit. Non-negative. Not applicable to discount items.
Assign the item to a category. The category must belong to your tenant. See Catalog categories.
Your cost for the item (owner-only field). Non-negative. Not applicable to bundle or discount items.
Markup percentage over cost (owner-only field). Non-negative. Not applicable to discount items.
URL to the supplier or part listing (owner-only field). Must be a valid URL. Not applicable to discount items.
Supplier's part number (owner-only field). Max 128 characters. Not applicable to discount items.
Required when kind is discount. One of percentage or flat.
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.
When true, the bundle is priced as a flat package rather than summing component prices. Only applicable to bundle items.
The items in the bundle. Required when kind is bundle; must contain at least one component.
The id of the catalog item to include in the bundle.
Default quantity for this component. Must be positive.
Display order for this component within the bundle. Defaults to 0.
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.
The catalog item to update.
Updated display name. Max 255 characters.
Updated description. Max 2,000 characters. Pass null to clear.
Updated SKU. Max 128 characters. Pass null to clear.
Updated unit label. Max 64 characters. Pass null to clear.
Updated customer price. Non-negative. Pass null to clear.
Assign or reassign the item to a category. The category must belong to your tenant. Pass null to clear the category.
Updated cost (owner-only). Non-negative. Pass null to clear.
Updated markup percentage (owner-only). Non-negative. Pass null to clear.
Updated supplier URL (owner-only). Must be a valid URL. Pass null to clear.
Updated supplier SKU (owner-only). Max 128 characters. Pass null to clear.
Updated discount type for discount items. One of percentage or flat. Required when updating discount_value.
Updated discount amount for discount items. Non-negative. Pass null to clear. When providing discount_value, you must also provide discount_type.
Toggle flat-package pricing on a bundle item. Pass null to clear.
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.
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.
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 type | What it means | Common cause |
|---|---|---|
invalid_input | A field failed validation, or a required actor condition was not met | Missing kind, missing discount_type on a discount item, missing components on a bundle, calling a write tool with a tenant-key token |
not_found | The referenced item does not exist in this tenant or is already archived | Stale or wrong id |
conflict | The request collides with existing state | Archiving an item that is still a component in an active bundle |
insufficient_scope | The token does not hold the required scope | Missing read:catalog_items or write:catalog_items |
For the full error shape and HTTP status codes, see Errors.