Catalog categories
Organize the pricebook into categories.
This page covers the five API tools for managing catalog categories in Zoop's pricebook. Categories let you group pricebook items — for example, separating plumbing labor from HVAC parts.
Categories can be nested: each category has an optional parent_id field that points to another category. If you do not need nesting, leave parent_id as null and every category sits at the top level.
When you call catalog_categories.list, Zoop returns every category as a flat array (one row per category, no nesting). Each row includes a parent_id so you can build the nested tree yourself in code. The tree-building example below shows how.
The five tools are list, get, create, update, and delete. All require a scope of either read:catalog_categories or write:catalog_categories — see Scopes below.
New to the Zoop API? Start at MCP overview to learn how to connect and authenticate before calling any tools.
Scopes
| Scope | Grants access to |
|---|---|
read:catalog_categories | catalog_categories.list, catalog_categories.get |
write:catalog_categories | catalog_categories.create, catalog_categories.update, catalog_categories.delete |
write:catalog_categories does not include read:catalog_categories. If your integration needs to read and write, grant both scopes.
The umbrella scopes read:catalog and write:catalog cover these child scopes too. For new integrations, use the specific child scopes (read:catalog_categories / write:catalog_categories) rather than the broader umbrella.
catalog_categories.list
Returns every category for your account as a flat array, ordered by sort_order ascending, then name ascending. Use the parent_id on each row to build a nested tree in your code — see Building a tree from flat rows.
This tool takes no input parameters.
Example
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "catalog_categories.list",
"arguments": {}
}
}
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "[{"id":"a1b2c3d4-0001-0000-0000-000000000000","tenant_id":"t-...","parent_id":null,"name":"Plumbing","description":null,"sort_order":0,"metadata":{},"created_at":"2026-01-10T09:00:00.000Z","updated_at":"2026-01-10T09:00:00.000Z"},{"id":"a1b2c3d4-0002-0000-0000-000000000000","tenant_id":"t-...","parent_id":"a1b2c3d4-0001-0000-0000-000000000000","name":"Drain & sewer","description":null,"sort_order":0,"metadata":{},"created_at":"2026-01-10T09:05:00.000Z","updated_at":"2026-01-10T09:05:00.000Z"}]"
}
]
}
}
catalog_categories.get
Fetch a single category by its id. Returns a not_found error if the ID does not exist in your account.
The id of the category to fetch.
Example
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "catalog_categories.get",
"arguments": {
"id": "a1b2c3d4-0002-0000-0000-000000000000"
}
}
}
catalog_categories.create
Create a new catalog category. The name must be unique among categories that share the same parent — two sibling categories cannot have the same name. Duplicate names at different levels of the tree are fine.
This tool requires a token linked to a specific user. API keys generated without a user (tenant-level keys) cannot call catalog_categories.create. Use a user-bound API key or an OAuth token instead. See Authentication for details.
Display name for the category. Max 255 characters. Must be unique among siblings (categories sharing the same parent_id).
ID of the parent category. Omit (or pass null) to create a root-level category. The parent must belong to the same tenant.
Optional description. Max 2,000 characters. Pass null to clear.
Controls the display order within a level. Lower numbers sort first. Defaults to 0. Categories with the same sort_order are then sorted by name.
Arbitrary key-value pairs you can attach for your own use. Values can be any JSON-serializable type.
Example — root category
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "catalog_categories.create",
"arguments": {
"name": "HVAC",
"description": "Heating, ventilation, and air conditioning",
"sort_order": 1
}
}
}
Example — child category
{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "catalog_categories.create",
"arguments": {
"name": "Filters & media",
"parent_id": "a1b2c3d4-0010-0000-0000-000000000000",
"sort_order": 0
}
}
}
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"content": [
{
"type": "text",
"text": "{"id":"a1b2c3d4-0011-0000-0000-000000000000","tenant_id":"t-...","parent_id":"a1b2c3d4-0010-0000-0000-000000000000","name":"Filters & media","description":null,"sort_order":0,"metadata":{},"created_at":"2026-06-13T14:00:00.000Z","updated_at":"2026-06-13T14:00:00.000Z"}"
}
]
}
}
catalog_categories.update
Update one or more fields on a category. This is a partial update — only the fields you include in the request are changed. Fields you omit stay as they are.
This tool requires a token linked to a specific user. API keys generated without a user (tenant-level keys) cannot call catalog_categories.update. Use a user-bound API key or an OAuth token instead.
You cannot move a category under one of its own descendants — that would create a loop. For example, if Plumbing is the parent of Drain & sewer, setting Plumbing's parent_id to Drain & sewer's ID is rejected with a conflict error.
The category to update.
New display name. Max 255 characters. Must be unique among siblings after the change.
New parent category ID. Pass null to move the category to the root. The new parent must belong to the same tenant and must not be a descendant of this category.
Updated description. Max 2,000 characters. Pass null to clear.
Updated sort position within this level.
Replaces the stored metadata object wholesale. Pass an empty object ({}) to clear all keys.
Example
{
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {
"name": "catalog_categories.update",
"arguments": {
"id": "a1b2c3d4-0002-0000-0000-000000000000",
"name": "Drain, sewer & jetting",
"sort_order": 1
}
}
}
catalog_categories.delete
Permanently delete a category. This cannot be undone.
Before removing the row, Zoop automatically:
- Moves the deleted category's direct children up one level to its parent. If the category was at the root, its children become new root categories.
- Clears the
category_idfield on any catalog items that were assigned to this category.
Both changes happen together in a single database transaction — you will never see a half-deleted state.
This is a permanent delete. There is no archive or recycle bin for categories. The category, its place in the tree, and all item assignments to it are gone for good. Double-check the id before calling this.
The id of the category to delete. Returns a not_found error if the ID does not exist in your account.
Example
{
"jsonrpc": "2.0",
"id": 6,
"method": "tools/call",
"params": {
"name": "catalog_categories.delete",
"arguments": {
"id": "a1b2c3d4-0002-0000-0000-000000000000"
}
}
}
{
"jsonrpc": "2.0",
"id": 6,
"result": {
"content": [
{
"type": "text",
"text": "{"deleted":true,"id":"a1b2c3d4-0002-0000-0000-000000000000"}"
}
]
}
}
Response fields
The list, get, create, and update tools all return category objects with the same fields.
Unique ID for the category.
The tenant this category belongs to.
The parent category's id, or null if this is a root category.
Display name.
Optional description text.
Sort position within this level of the tree. Lower numbers sort first.
Arbitrary key-value pairs. Empty object if none were set.
When the category was created.
When the category was last updated.
Building a tree from flat rows
catalog_categories.list returns a flat array — every category in one list, not nested. Each row has an id and a parent_id. The snippet below groups them into a nested tree you can render in a UI or traverse in code:
type Category = {
id: string
parent_id: string | null
name: string
sort_order: number
children?: Category[]
}
function buildTree(rows: Category[]): Category[] {
const byId = new Map(rows.map((r) => [r.id, { ...r, children: [] as Category[] }]))
const roots: Category[] = []
for (const row of byId.values()) {
if (row.parent_id && byId.has(row.parent_id)) {
byId.get(row.parent_id)!.children!.push(row)
} else {
roots.push(row)
}
}
// Sort each level by sort_order, then name (server already ordered this way,
// but re-sorting after grouping is cheap and makes the result deterministic).
function sort(nodes: Category[]) {
nodes.sort((a, b) => a.sort_order - b.sort_order || a.name.localeCompare(b.name))
nodes.forEach((n) => n.children && sort(n.children))
}
sort(roots)
return roots
}
Error reference
| Error | What it means | What to check |
|---|---|---|
invalid_input | A field failed validation, or the token is not user-linked | Is name present? Is parent_id a valid UUID? Are you using a user-bound token for create/update? |
not_found | The category ID does not exist in your account | Is the id correct and still current? |
conflict | The request would break a uniqueness or tree rule | Duplicate name under the same parent, or moving a category under one of its own descendants |
insufficient_scope | The token lacks the required scope | Add read:catalog_categories or write:catalog_categories to the token's scopes |
For the full error shape and HTTP status codes, see Errors.