# Categories

Categories group the products of a store. They are two levels deep: a top-level category can hold subcategories, and a subcategory cannot hold any. These six endpoints list, read, create, change, delete and reorder them.

A product joins a category through its `category_id` field, see [Products](https://dzbuild.com/api-docs/resources/products.md). A `category-products` section of the home page takes a category id too, see [Home page sections](https://dzbuild.com/api-docs/resources/home-layout.md).

## Before you start

* Reads need `products:read` and writes need `products:write`, the same scopes as products. Merchant keys carry both.
* `POST`, `PATCH` and `DELETE` need an `Idempotency-Key`. See [Idempotency](https://dzbuild.com/api-docs/idempotency.md).
* The category image is added in the dashboard. The API returns its URL but cannot upload or change it.

## The category object

| Field                | Type           | Notes                                                                                                                                                                                                                                                                                                                                 |
| -------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                 | int            | The category id.                                                                                                                                                                                                                                                                                                                      |
| `name`               | string         | 1 to 100 characters.                                                                                                                                                                                                                                                                                                                  |
| `slug`               | string         | Made from the name and unique in the store. The category's page on the store uses it in its address.                                                                                                                                                                                                                                  |
| `description`        | string or null | Free text.                                                                                                                                                                                                                                                                                                                            |
| `image`              | string or null | Full URL of the image, or `null`.                                                                                                                                                                                                                                                                                                     |
| `parent_id`          | int or null    | The parent category, or `null` for a top-level category.                                                                                                                                                                                                                                                                              |
| `show_subcategories` | bool           | Shows the subcategories as tiles on the category's page in the store (**Show subcategories section** on the category form). Stored as `true` for a subcategory. While **Subcategories only inside the parent category** is on (`/dashboard/categories`), the store shows the tiles on every category's page whatever this field says. |
| `sort_order`         | int            | Position in the store's category lists, lowest first.                                                                                                                                                                                                                                                                                 |
| `status`             | string         | `active` or `inactive`.                                                                                                                                                                                                                                                                                                               |
| `created_at`         | string         | `YYYY-MM-DD HH:MM:SS`, server time.                                                                                                                                                                                                                                                                                                   |

The list and the single read add `product_count`, the number of products in the category. The single read also adds `children`, its subcategories.

## `GET /v1/categories`

The store's categories, newest first, as a cursor list. Sort them by `sort_order` to get the store's order.

**Auth:** platform key with `products:read`.

### Query parameters

| Param       | Type                   | Default | Notes                                                                                                                                             |
| ----------- | ---------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `parent_id` | id, `0` or `null`      | none    | A category id returns its subcategories. `0`, `null` or an empty value returns the top-level categories. Anything else returns `400 bad_request`. |
| `status`    | `active` or `inactive` | none    | Only the categories with this status. Any other value is ignored.                                                                                 |
| `limit`     | int                    | 50      | 1 to 200.                                                                                                                                         |
| `cursor`    | string                 | none    | `next_cursor` of the previous page. See [Pagination](https://dzbuild.com/api-docs/pagination.md).                                                 |

### Request

```
curl 'https://api.dzbuild.app/v1/categories?parent_id=0' \

  -H "Authorization: Bearer $DZ_KEY"
```

### Response 200

```
{

  "data": {

    "items": [

      {

        "id": 14,

        "name": "Montres",

        "slug": "montres",

        "description": null,

        "image": null,

        "parent_id": null,

        "show_subcategories": true,

        "sort_order": 4,

        "status": "active",

        "created_at": "2026-09-30 11:20:05",

        "product_count": 0

      },

      {

        "id": 10,

        "name": "Parfums",

        "slug": "parfums",

        "description": "Eaux de parfum et coffrets",

        "image": null,

        "parent_id": null,

        "show_subcategories": true,

        "sort_order": 1,

        "status": "active",

        "created_at": "2026-09-12 09:41:37",

        "product_count": 18

      }

    ],

    "next_cursor": null,

    "has_more": false

  }

}
```

## `GET /v1/categories/{id}`

One category with `product_count` and `children`, its subcategories ordered by `sort_order`.

**Auth:** platform key with `products:read`.

An id that is not all digits answers `400 bad_request`. A category of another store answers `404 not_found`, like one that does not exist.

### Request

```
curl 'https://api.dzbuild.app/v1/categories/10' \

  -H "Authorization: Bearer $DZ_KEY"
```

### Response 200

```
{

  "data": {

    "id": 10,

    "name": "Parfums",

    "slug": "parfums",

    "description": "Eaux de parfum et coffrets",

    "image": null,

    "parent_id": null,

    "show_subcategories": true,

    "sort_order": 1,

    "status": "active",

    "created_at": "2026-09-12 09:41:37",

    "children": [

      {"id": 11, "name": "Parfums femme", "slug": "parfums-femme", "sort_order": 2, "status": "active"},

      {"id": 12, "name": "Parfums homme", "slug": "parfums-homme", "sort_order": 3, "status": "active"}

    ],

    "product_count": 18

  }

}
```

## `POST /v1/categories`

Creates a category and places it last: its `sort_order` is one more than the highest in the store.

**Auth:** platform key with `products:write`. **Requires `Idempotency-Key`.**

### Body

| Field                | Type                   | Required | Notes                                                                                                                                  |
| -------------------- | ---------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `name`               | string                 | yes      | Trimmed, 1 to 100 characters.                                                                                                          |
| `description`        | string or null         | no       | Trimmed. An empty string is stored as `null`.                                                                                          |
| `parent_id`          | int or null            | no       | A top-level category of this store: the new category becomes its subcategory. `null` or `0` makes a top-level category.                |
| `status`             | `active` or `inactive` | no       | Defaults to `active`.                                                                                                                  |
| `show_subcategories` | bool                   | no       | Defaults to `true`. Ignored, and stored as `true`, for a subcategory or while **Subcategories only inside the parent category** is on. |

The `slug` is made from the name: lowercase, letters and digits kept, and any run of other characters turned into one `-`. An Arabic name keeps its Arabic letters. When another category of the store already has that `slug`, `-2`, `-3` and so on is added.

### Request

```
curl -X POST 'https://api.dzbuild.app/v1/categories' \

  -H "Authorization: Bearer $DZ_KEY" \

  -H "Content-Type: application/json" \

  -H "Idempotency-Key: cat-create-coffrets-1" \

  -d '{"name": "Coffrets cadeaux", "parent_id": 10}'
```

### Response 201

The answer is the category object, without `product_count` and `children`.

```
{

  "data": {

    "id": 15,

    "name": "Coffrets cadeaux",

    "slug": "coffrets-cadeaux",

    "description": null,

    "image": null,

    "parent_id": 10,

    "show_subcategories": true,

    "sort_order": 5,

    "status": "active",

    "created_at": "2026-10-06 14:02:11"

  }

}
```

## `PATCH /v1/categories/{id}`

Changes only the fields you send. The body takes the fields of `POST`, all optional.

**Auth:** platform key with `products:write`. **Requires `Idempotency-Key`.**

* A new `name` gives a new `slug`, so the address of the category's page on the store changes and links to the old address stop working. Sending the same name keeps the `slug`.
* `parent_id` moves the category. A top-level category id makes it a subcategory, and `null` or `0` makes it top-level. A category that has subcategories cannot become a subcategory, and a category cannot be its own parent.
* An empty body changes nothing and answers the category as it is.

### Request

```
curl -X PATCH 'https://api.dzbuild.app/v1/categories/15' \

  -H "Authorization: Bearer $DZ_KEY" \

  -H "Content-Type: application/json" \

  -H "Idempotency-Key: cat-15-move-1" \

  -d '{"parent_id": null, "status": "inactive"}'
```

### Response 200

```
{

  "data": {

    "id": 15,

    "name": "Coffrets cadeaux",

    "slug": "coffrets-cadeaux",

    "description": null,

    "image": null,

    "parent_id": null,

    "show_subcategories": true,

    "sort_order": 5,

    "status": "inactive",

    "created_at": "2026-10-06 14:02:11"

  }

}
```

## `DELETE /v1/categories/{id}`

Deletes an empty category and its image.

**Auth:** platform key with `products:write`. **Requires `Idempotency-Key`.**

A category that still holds products or subcategories answers `409 category_not_empty`, and nothing is deleted. The error message gives the counts. To empty the category:

* Subcategories: move each one to the top level with `PATCH` and `"parent_id": null`, or delete it first.
* Products: the API cannot take a product out of a category. Setting a product's `category_id` adds a category and keeps the ones the product already has, see [Products](https://dzbuild.com/api-docs/resources/products.md). Change the categories of those products in the dashboard.

### Request

```
curl -X DELETE 'https://api.dzbuild.app/v1/categories/14' \

  -H "Authorization: Bearer $DZ_KEY" \

  -H "Idempotency-Key: cat-14-delete-1"
```

### Response 200

```
{

  "data": {

    "deleted": true,

    "id": 14

  }

}
```

## `POST /v1/categories/reorder`

Sets the `sort_order` of the categories you list, all in one step: either every one of them changes or none does.

**Auth:** platform key with `products:write`. **Requires `Idempotency-Key`.**

### Body

| Field        | Type  | Required | Notes                                                                                                                  |
| ------------ | ----- | -------- | ---------------------------------------------------------------------------------------------------------------------- |
| `categories` | array | yes      | The categories in their new order. Each item is `{"id": 10}`, `{"id": 10, "sort_order": 7}` or a bare id such as `10`. |

* An item without `sort_order` gets its position in the array: 1, 2, 3 and so on. An item with `sort_order` gets that number.
* Each id must belong to the store and appear once.
* The categories you leave out keep their `sort_order`.
* A reorder is not recorded in the change history, so it cannot be undone. Read the list first if you may want the old order back.

### Request

```
curl -X POST 'https://api.dzbuild.app/v1/categories/reorder' \

  -H "Authorization: Bearer $DZ_KEY" \

  -H "Content-Type: application/json" \

  -H "Idempotency-Key: cat-reorder-1" \

  -d '{"categories": [{"id": 14}, {"id": 10}]}'
```

### Response 200

```
{

  "data": {

    "reordered": 2,

    "categories": [

      {"id": 14, "sort_order": 1},

      {"id": 10, "sort_order": 2}

    ]

  }

}
```

## Undo

`POST`, `PATCH` and `DELETE` are recorded in the store's change history. Their answer does not carry the change id: `GET /v1/changes?entity=category` lists the category changes, newest first, with `store:read`. `entity_id` is the category id.

### Request

```
curl 'https://api.dzbuild.app/v1/changes?entity=category&limit=1' \

  -H "Authorization: Bearer $DZ_KEY"
```

### Response 200

```
{

  "data": {

    "items": [

      {

        "id": 120,

        "entity": "category",

        "entity_id": "15",

        "action": "update",

        "summary": "Updated category #15 (parent_id, status, show_subcategories)",

        "undone_at": null,

        "created_at": "2026-10-06 14:05:48",

        "undone": false

      }

    ],

    "next_cursor": "MTIw",

    "has_more": true

  }

}
```

`POST /v1/changes/{id}/undo` reverts one change. It needs `products:write` and an `Idempotency-Key`.

* Undoing a `PATCH` puts back the earlier values of the fields that call changed, with the same checks as `PATCH`.
* Undoing a `DELETE` creates the category again under its old id, without its image. When its old parent is gone or has become a subcategory, the undo answers `422 invalid_parent`.
* A create cannot be undone: the undo answers `422 nothing_to_restore`. Delete the category instead.
* When the category is gone, or its old id is taken, the undo answers `422 restore_target_missing`.
* A change undone a second time answers `409 already_undone`.
* Changes made in the dashboard are not recorded, so they cannot be undone through the API.
* An installed app's token cannot read or undo changes: both answer `403 forbidden` (`Apps cannot use this endpoint`).

```
curl -X POST 'https://api.dzbuild.app/v1/changes/120/undo' \

  -H "Authorization: Bearer $DZ_KEY" \

  -H "Idempotency-Key: undo-120"
```

```
{

  "data": {

    "undone": true,

    "change_id": 120,

    "entity": "category",

    "undo_change_id": 121

  }

}
```

## Errors

| HTTP | Code                     | Cause                                                                                                                                                                                                                                                                                                            |
| ---- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400  | `bad_request`            | The id in the path is not all digits, the body is not valid JSON, the `parent_id` filter is not an id, `0`, `null` or empty, `categories` is missing or not an array, or `Idempotency-Key` is missing or malformed.                                                                                              |
| 401  | `unauthorized`           | Bad or missing key.                                                                                                                                                                                                                                                                                              |
| 402  | `quota_exceeded`         | The store's monthly request quota is used up. See [Rate limits](https://dzbuild.com/api-docs/rate-limits.md).                                                                                                                                                                                                    |
| 403  | `forbidden`              | `Missing scope: products:read`, `Missing scope: products:write` or `Missing scope: store:read`, or `API access requires an active Enterprise plan` for a merchant key whose store is not on an active Enterprise plan, or `Apps cannot use this endpoint` when an installed app's token reads or undoes changes. |
| 404  | `not_found`              | No category with this id in the store, a reorder lists an id that is not in the store, or an undo names a change that is not in the store's history.                                                                                                                                                             |
| 409  | `category_not_empty`     | The category still holds products or subcategories.                                                                                                                                                                                                                                                              |
| 409  | `already_undone`         | Undo only: the change was already undone.                                                                                                                                                                                                                                                                        |
| 413  | `payload_too_large`      | The body is over 1 MB.                                                                                                                                                                                                                                                                                           |
| 422  | `validation_error`       | `name` is missing, empty or over 100 characters, `status` is not `active` or `inactive`, `parent_id` is not an id, or a reorder is empty, repeats an id, has an id that is not a positive whole number or a `sort_order` that is not a whole number.                                                             |
| 422  | `invalid_parent`         | `parent_id` is not a category of this store, is itself a subcategory, is the category itself, or the category has subcategories and cannot move under another one.                                                                                                                                               |
| 422  | `nothing_to_restore`     | Undo only: the change created the category.                                                                                                                                                                                                                                                                      |
| 422  | `restore_target_missing` | Undo only: the category is gone, or its old id is taken.                                                                                                                                                                                                                                                         |
| 422  | `idempotency_key_reuse`  | The same `Idempotency-Key` was used with a different body.                                                                                                                                                                                                                                                       |
| 429  | `rate_limited`           | Too many calls in the current minute. Wait for the `Retry-After` seconds. See [Rate limits](https://dzbuild.com/api-docs/rate-limits.md).                                                                                                                                                                        |
| 500  | `server_error`           | The request failed. Retry with the same `Idempotency-Key`.                                                                                                                                                                                                                                                       |
