> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kaireonai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Categories API

> Create, update, list, and delete categories and sub-categories in the business hierarchy.

<Frame caption="The Business Hierarchy page where categories live.">
  <img src="https://mintcdn.com/kaireonai/l-jsUQlUEuA3B6hG/images/screenshots/categories-list.png?fit=max&auto=format&n=l-jsUQlUEuA3B6hG&q=85&s=4c20838331473268ed860f2f6c956162" alt="Business Hierarchy page showing categories and sub-categories" width="1440" height="900" data-path="images/screenshots/categories-list.png" />
</Frame>

Categories form the top level of the business hierarchy. Each category can contain sub-categories, which in turn group offers. Categories also define custom fields (including computed fields with formulas) that are inherited by all offers within the category.

All categories support **soft-delete** (a `deletedAt` timestamp is set instead of permanent removal), **version tracking** (the `version` field auto-increments on every update), and **audit logging** (before/after snapshots are recorded for every CRUD operation).

<Info>
  See the [Business Hierarchy feature page](/studio/business-hierarchy) for UI guidance and conceptual overview.
</Info>

## Base path

```
/api/v1/categories        # Categories
/api/v1/sub-categories    # Sub-categories
```

***

# Categories

## List categories

```
GET /api/v1/categories
```

Returns a paginated list of categories for the current tenant, ordered by `ordinal` ascending. Each category includes its sub-categories with offer counts. By default, soft-deleted categories are excluded.

### Query parameters

| Parameter        | Required | Type    | Description                                                              |
| ---------------- | -------- | ------- | ------------------------------------------------------------------------ |
| `limit`          | No       | integer | Maximum results per page. Default `50`, max `100`.                       |
| `cursor`         | No       | string  | Cursor for keyset pagination. Pass the last `id` from the previous page. |
| `includeDeleted` | No       | string  | Set to `"true"` to include soft-deleted categories in the results.       |

### Response `200`

```json theme={null}
{
  "data": [
    {
      "id": "cat_01",
      "tenantId": "t_001",
      "name": "Acquisition",
      "description": "New customer acquisition offers.",
      "icon": "user-plus",
      "color": "blue",
      "status": "active",
      "ordinal": 0,
      "version": 1,
      "deletedAt": null,
      "customFields": [
        {
          "name": "base_rate",
          "type": "number",
          "required": false
        },
        {
          "name": "personalized_rate",
          "type": "computed",
          "formula": "base_rate * (1 + customer.loyalty_score / 100)",
          "outputType": "number",
          "required": false
        }
      ],
      "subCategories": [
        {
          "id": "sub_01",
          "name": "Credit Cards",
          "_count": { "offers": 12 }
        }
      ],
      "createdAt": "2026-03-10T12:00:00.000Z",
      "updatedAt": "2026-03-12T09:30:00.000Z"
    }
  ],
  "pagination": {
    "limit": 50,
    "cursor": null,
    "hasMore": false,
    "total": 4
  }
}
```

### Error codes

| Code  | Reason                                |
| ----- | ------------------------------------- |
| `401` | Missing or invalid API key / session. |
| `403` | Insufficient role.                    |

***

## Create a category

```
POST /api/v1/categories
```

### Request body

| Field          | Required | Type           | Description                                          |
| -------------- | -------- | -------------- | ---------------------------------------------------- |
| `name`         | **Yes**  | string (1-255) | Unique category name.                                |
| `description`  | No       | string         | Category description.                                |
| `icon`         | No       | string         | Icon identifier (e.g., Lucide icon name).            |
| `color`        | No       | string         | Display color. Default `"blue"`.                     |
| `status`       | No       | enum           | `draft`, `active` (default), `paused`, `archived`.   |
| `ordinal`      | No       | integer (>= 0) | Sort order. Default `0`.                             |
| `customFields` | No       | array          | Custom field definitions (see below). Max 500 items. |

#### Custom field object

| Field        | Required    | Type      | Description                                                                                                                       |
| ------------ | ----------- | --------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `name`       | **Yes**     | string    | Machine-readable field name. Used as the field key on offers and as the variable name inside formulas of sibling computed fields. |
| `type`       | **Yes**     | string    | Field type: `text`, `number`, `boolean`, `select`, `date`, `computed`.                                                            |
| `required`   | No          | boolean   | Whether offers must populate this field. Default `false`.                                                                         |
| `options`    | Conditional | string\[] | Required when `type` is `select` — the list of allowed values.                                                                    |
| `formula`    | Conditional | string    | Formula expression. **Required when `type` is `computed`**.                                                                       |
| `outputType` | Conditional | enum      | `number` or `text`. **Required when `type` is `computed`**.                                                                       |

<Warning>
  Computed fields require both a valid `formula` and an `outputType` (`number` or `text`). The formula is validated using the [formula engine](/tutorials/formula-reference). Supported namespaces: `fieldName` (sibling fields), `customer.*` (enriched data), `attributes.*` (request-time attributes).

  **The formula is compiled at write time, on both `POST` and `PUT`.** A formula that does not parse is rejected with a `400` naming the field index and the parse error — it is not accepted and deferred. This matters because the alternative is failing once per candidate inside the decision hot path, on live traffic, long after whoever typed it has moved on.
</Warning>

<Warning>
  **`customFields` is replaced, not merged.** A `PUT` that includes `customFields` overwrites the entire array. Send the complete set of fields you want the category to end up with — omitting one deletes it, including a `PUT` whose only intent was to change a single formula.
</Warning>

### Example request

```json theme={null}
{
  "name": "Retention",
  "description": "Offers aimed at retaining existing customers.",
  "color": "green",
  "ordinal": 1,
  "customFields": [
    { "name": "discount_pct", "type": "number", "required": false },
    {
      "name": "final_discount",
      "type": "computed",
      "formula": "discount_pct * (customer.tenure_years > 5 ? 1.2 : 1.0)",
      "outputType": "number",
      "required": false
    }
  ]
}
```

### Response `201`

Returns the created category with `version: 1`, `deletedAt: null`, and sub-categories relation. An audit log entry is created with a `create` action and a snapshot of the new entity.

### Error codes

| Code  | Reason                                                   |
| ----- | -------------------------------------------------------- |
| `400` | Validation error (missing name, invalid computed field). |
| `401` | Missing or invalid API key / session.                    |
| `403` | Insufficient role (requires `editor` or `admin`).        |
| `409` | A category with that name already exists.                |
| `413` | Request body exceeds the 2 MB limit.                     |
| `415` | `Content-Type` is not `application/json`.                |

***

## Update a category

```
PUT /api/v1/categories
```

Updates an existing category. Only provided fields are changed. The `version` field is auto-incremented and a before/after audit snapshot is recorded.

### Request body

| Field | Required | Type   | Description            |
| ----- | -------- | ------ | ---------------------- |
| `id`  | **Yes**  | string | Category ID to update. |

All other fields from the create schema are accepted as optional.

### Response `200`

Returns the updated category object with the incremented `version`.

### Error codes

| Code  | Reason                                               |
| ----- | ---------------------------------------------------- |
| `400` | Validation error.                                    |
| `401` | Missing or invalid API key / session.                |
| `403` | Insufficient role.                                   |
| `404` | No category with that `id` (or name) in your tenant. |
| `409` | A category with that name already exists.            |
| `413` | Request body exceeds the 2 MB limit.                 |
| `415` | `Content-Type` is not `application/json`.            |

***

## Delete a category (soft-delete)

```
DELETE /api/v1/categories?id={categoryId}
```

Soft-deletes a category by setting its `deletedAt` timestamp. **Cascade behavior:** all child sub-categories and offers under this category are also soft-deleted. The `version` is incremented on the category and each cascaded child. An audit log entry is recorded for every affected entity.

### Query parameters

| Parameter | Required | Type   | Description            |
| --------- | -------- | ------ | ---------------------- |
| `id`      | **Yes**  | string | Category ID to delete. |

### Response `200`

```json theme={null}
{
  "deleted": true,
  "id": "cat_01",
  "cascaded": 5,
  "message": "Category soft-deleted along with 5 child entities."
}
```

### Error codes

| Code  | Reason                                                            |
| ----- | ----------------------------------------------------------------- |
| `400` | Missing `id` query parameter, or the category is already deleted. |
| `401` | Missing or invalid API key / session.                             |
| `403` | Insufficient role.                                                |
| `404` | No category with that `id` (or name) in your tenant.              |

<Note>
  To restore a soft-deleted category, use `POST /api/v1/restore?entityType=category&id={categoryId}` (admin only). Restoring a category does not automatically restore cascaded children -- you must restore sub-categories and offers individually.
</Note>

***

# Sub-categories

Sub-categories live under a category and group related offers. They also support soft-delete, version tracking, and audit logging.

## List sub-categories

```
GET /api/v1/sub-categories
```

### Query parameters

| Parameter        | Required | Type    | Description                                                              |
| ---------------- | -------- | ------- | ------------------------------------------------------------------------ |
| `categoryId`     | No       | string  | Filter to a specific parent category.                                    |
| `limit`          | No       | integer | Maximum results per page. Default `50`, max `100`.                       |
| `cursor`         | No       | string  | Cursor for keyset pagination. Pass the last `id` from the previous page. |
| `includeDeleted` | No       | string  | Set to `"true"` to include soft-deleted sub-categories.                  |

### Response `200`

```json theme={null}
{
  "data": [
    {
      "id": "sub_01",
      "tenantId": "t_001",
      "categoryId": "cat_01",
      "name": "Credit Cards",
      "description": "",
      "icon": "",
      "status": "active",
      "ordinal": 0,
      "version": 1,
      "deletedAt": null,
      "customFields": [],
      "category": { "id": "cat_01", "name": "Acquisition" },
      "_count": { "offers": 12 }
    }
  ],
  "pagination": {
    "limit": 50,
    "cursor": null,
    "hasMore": false,
    "total": 3
  }
}
```

***

## Create a sub-category

```
POST /api/v1/sub-categories
```

### Request body

| Field          | Required | Type           | Description                                           |
| -------------- | -------- | -------------- | ----------------------------------------------------- |
| `categoryId`   | **Yes**  | string         | Parent category ID.                                   |
| `name`         | **Yes**  | string         | Sub-category name.                                    |
| `description`  | No       | string         | Description.                                          |
| `icon`         | No       | string         | Icon identifier.                                      |
| `status`       | No       | enum           | `draft`, `active` (default), `paused`, `archived`.    |
| `ordinal`      | No       | integer (>= 0) | Sort order. Default `0`.                              |
| `customFields` | No       | array          | Custom field definitions (same schema as categories). |

### Response `201`

Returns the created sub-category with its parent category relation, `version: 1`, and `deletedAt: null`.

### Error codes

| Code  | Reason                                                                                                                                       |
| ----- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `400` | Validation error (missing `categoryId` or `name`), or a `categoryId` that doesn't exist in your tenant (foreign-key ownership is validated). |
| `401` | Missing or invalid API key / session.                                                                                                        |
| `403` | Insufficient role.                                                                                                                           |
| `409` | A sub-category with that name already exists.                                                                                                |
| `413` | Request body exceeds the 2 MB limit.                                                                                                         |
| `415` | `Content-Type` is not `application/json`.                                                                                                    |

***

## Update a sub-category

```
PUT /api/v1/sub-categories
```

Updates an existing sub-category. The `version` field is auto-incremented and a before/after audit snapshot is recorded.

### Request body

| Field | Required | Type   | Description                |
| ----- | -------- | ------ | -------------------------- |
| `id`  | **Yes**  | string | Sub-category ID to update. |

All other fields are optional.

### Response `200`

Returns the updated sub-category object with the incremented `version`.

### Error codes

| Code  | Reason                                                   |
| ----- | -------------------------------------------------------- |
| `400` | Validation error or missing `id`.                        |
| `401` | Missing or invalid API key / session.                    |
| `403` | Insufficient role.                                       |
| `404` | No sub-category with that `id` (or name) in your tenant. |
| `409` | A sub-category with that name already exists.            |
| `413` | Request body exceeds the 2 MB limit.                     |
| `415` | `Content-Type` is not `application/json`.                |

***

## Delete a sub-category (soft-delete)

```
DELETE /api/v1/sub-categories?id={subCategoryId}
```

Soft-deletes a sub-category by setting its `deletedAt` timestamp. The `version` is incremented.

### Query parameters

| Parameter | Required | Type   | Description                |
| --------- | -------- | ------ | -------------------------- |
| `id`      | **Yes**  | string | Sub-category ID to delete. |

### Response `200`

```json theme={null}
{
  "success": true
}
```

### Error codes

| Code  | Reason                                                   |
| ----- | -------------------------------------------------------- |
| `400` | Missing `id`, or soft-delete failed.                     |
| `401` | Missing or invalid API key / session.                    |
| `403` | Insufficient role.                                       |
| `404` | No sub-category with that `id` (or name) in your tenant. |

***

## Role requirements

| Method | Minimum role |
| ------ | ------------ |
| GET    | `viewer`     |
| POST   | `editor`     |
| PUT    | `editor`     |
| DELETE | `editor`     |

<Card title="Business Hierarchy" icon="sitemap" href="/studio/business-hierarchy">
  Learn more about organising categories and sub-categories in the platform UI.
</Card>
