API referenceCatalog categories

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

ScopeGrants access to
read:catalog_categoriescatalog_categories.list, catalog_categories.get
write:catalog_categoriescatalog_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.

body
id

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.

body
name

Display name for the category. Max 255 characters. Must be unique among siblings (categories sharing the same parent_id).

body
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.

body
description

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

body
sort_order

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.

body
metadata

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.

body
id

The category to update.

body
name

New display name. Max 255 characters. Must be unique among siblings after the change.

body
parent_id

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.

body
description

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

body
sort_order

Updated sort position within this level.

body
metadata

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:

  1. 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.
  2. Clears the category_id field 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.

body
id

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.

id

Unique ID for the category.

tenant_id

The tenant this category belongs to.

parent_id

The parent category's id, or null if this is a root category.

name

Display name.

description

Optional description text.

sort_order

Sort position within this level of the tree. Lower numbers sort first.

metadata

Arbitrary key-value pairs. Empty object if none were set.

created_at

When the category was created.

updated_at

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

ErrorWhat it meansWhat to check
invalid_inputA field failed validation, or the token is not user-linkedIs name present? Is parent_id a valid UUID? Are you using a user-bound token for create/update?
not_foundThe category ID does not exist in your accountIs the id correct and still current?
conflictThe request would break a uniqueness or tree ruleDuplicate name under the same parent, or moving a category under one of its own descendants
insufficient_scopeThe token lacks the required scopeAdd read:catalog_categories or write:catalog_categories to the token's scopes

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