# Store menus

The store menu is the merchant's choice of links for the navbar, the phone side menu and the footer. The API reads and writes it as one document: up to 12 top links with up to 10 sub-links each, up to 3 footer groups of 10 links, and one switch for the wide category panel. A store that never saved a menu shows the automatic links, and these two endpoints answer `automatic: true` for it.

The merchant edits the same document in the dashboard, in Customize, row **Menu et liens / القائمة والروابط**. See [Menu and footer links](https://dzbuild.com/docs/storefront/menus) in the merchant docs.

## Before you start

* Reads need `store:read` and writes need `store:write`. Merchant keys carry both.
* `PUT` replaces the whole document. Read it first, change what you need, and send it back with the `version` you read.
* `Idempotency-Key` is optional on `PUT`. A keyed `PUT` is replayed for 24 hours. See [Idempotency](https://dzbuild.com/api-docs/idempotency.md).

## The menu document

| Field    | Type            | Notes                                                                                                                                                         |
| -------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `v`      | int             | Always `1`.                                                                                                                                                   |
| `mega`   | boolean         | `true` turns on the wide category panel. See [The wide panel](#the-wide-panel).                                                                               |
| `top`    | array of items  | The navbar links, in order. 1 to 12. `home`, `products`, `categories`, `track`, `contact` and `cart` appear at most once each here; children are not counted. |
| `footer` | array of groups | The footer columns, in order. Up to 3.                                                                                                                        |

### Items

| Field                | Type           | Notes                                                                                                                                                    |
| -------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`               | string         | One of `home`, `products`, `categories`, `category`, `page`, `track`, `contact`, `cart`, `url`. See [Item types](#item-types).                           |
| `label`              | string         | Up to 40 characters. Empty means the target's own name, in the visitor's language for the built-in types. Required for `url`.                            |
| `ref`                | int or null    | The category id for `category`, the page id for `page`, `null` for the other types.                                                                      |
| `url`                | string         | The address for `url`, up to 500 bytes. Empty for the other types.                                                                                       |
| `show_subcategories` | boolean        | Top-level `category` items only: also shows up to 10 subcategories of that category. Stored `false` everywhere else, children and footer links included. |
| `children`           | array of items | Top items only: up to 10 sub-links. A sub-link has no `children` of its own and cannot be a `categories` item.                                           |

### Groups

| Field   | Type           | Notes                                                                                                                                                                                              |
| ------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `key`   | string or null | `quick_links` or `categories` for the two automatic groups, `null` for a group you add.                                                                                                            |
| `label` | string         | The column title, up to 40 characters. It may be empty only when `key` is set: the store then shows the automatic title in the visitor's language. A typed title shows the same in every language. |
| `links` | array of items | 1 to 10 links. Footer links have no `children` and no `show_subcategories`.                                                                                                                        |

Labels are stored as plain text: HTML tags are removed and spaces are trimmed. The whole document is at most 16 KB.

### Item types

| `type`       | The link opens                                                                                                                                       |
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `home`       | `/`, the store home.                                                                                                                                 |
| `products`   | `/products`, all products.                                                                                                                           |
| `categories` | A drop-down list of the main categories and their subcategories, with an all-products row. In the footer, the categories part of the home page.      |
| `category`   | `/category/{ref}`. `ref` must be a category of this store.                                                                                           |
| `page`       | `/pages/{slug}` of the page `ref`. `ref` must be a published page of this store. See [Store pages](https://dzbuild.com/api-docs/resources/pages.md). |
| `track`      | `/track-order`.                                                                                                                                      |
| `contact`    | The contact part of the home page.                                                                                                                   |
| `cart`       | `/cart`.                                                                                                                                             |
| `url`        | `url`: a store path such as `/pages/delivery`, an anchor such as `#products`, an `http://` or `https://` address, `tel:` or `mailto:`.               |

A `url` without a scheme that looks like a domain, such as `instagram.com/x`, is stored as `https://instagram.com/x`. `javascript:`, `data:`, `//`, backslashes, quotes, spaces and angle brackets are refused with `bad_url`.

On the store, a link to a category or a page that was deleted since, or a page turned back into a draft, is left out with its sub-links. An inactive category is not shown.

### The wide panel

With `"mega": true`, a top item whose sub-links have their own sub-links shows on computers as a wide panel, one column per sub-link. The `categories` item does when a main category has subcategories. A `category` item with `show_subcategories` lists up to 10 subcategories, and with `mega` up to 10 of their own subcategories each, a third level. The `categories` item keeps the caps of the automatic menu: 10 main categories with 5 subcategories each in the navbar, 12 with 6 on the Promobi theme. On phones the side menu shows the same levels as an accordion.

## `GET /v1/store/menus`

The store's menu. For a store that never saved one, `menus` is the automatic menu and `automatic` is `true`.

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

### Request

```
curl 'https://api.dzbuild.app/v1/store/menus' \

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

### Response 200

`version` is the token to send back with `PUT`. `max` gives the limits and `types` the accepted item types.

```
{

  "data": {

    "automatic": false,

    "menus": {

      "v": 1,

      "mega": false,

      "top": [

        {"type": "home", "label": "", "ref": null, "url": "", "show_subcategories": false, "children": []},

        {"type": "category", "label": "", "ref": 152, "url": "", "show_subcategories": true, "children": []},

        {"type": "page", "label": "", "ref": 7, "url": "", "show_subcategories": false, "children": []}

      ],

      "footer": [

        {"label": "Infos", "links": [{"type": "track", "label": "", "ref": null, "url": ""}, {"type": "url", "label": "Instagram", "ref": null, "url": "https://instagram.com/my-store"}]}

      ]

    },

    "version": "3f9a1c07d2e4b851",

    "max": {"top": 12, "children": 10, "groups": 3, "links": 10, "label": 40},

    "types": ["home", "products", "category", "page", "track", "contact", "cart", "url", "categories"]

  },

  "meta": {"request_id": "5d2a8e1f07c3b946", "api_version": "v1"}

}
```

## `PUT /v1/store/menus`

Replaces the whole menu.

**Auth:** platform key with `store:write`. `Idempotency-Key` optional.

### Body

| Field     | Type           | Required | Notes                                                                                                                                                             |
| --------- | -------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `menus`   | object or null | yes      | The new document. `null` brings back the automatic menu.                                                                                                          |
| `version` | string         | no       | The `version` you read. When it no longer matches, the call answers `409 write_conflict` and changes nothing. Without it, the call overwrites whatever is stored. |

### Request

```
curl -X PUT 'https://api.dzbuild.app/v1/store/menus' \

  -H "Authorization: Bearer $DZ_KEY" \

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

  -H "Idempotency-Key: menus-sizes-1" \

  -d '{"version": "3f9a1c07d2e4b851", "menus": {"v": 1, "mega": false, "top": [{"type": "home", "label": "", "ref": null, "url": "", "show_subcategories": false, "children": []}, {"type": "page", "label": "Guide des tailles", "ref": 9, "url": "", "show_subcategories": false, "children": []}], "footer": []}}'
```

### Response 200

`menus` is the document as stored, `version` the new token and `change_id` the change log id. A body that changes nothing answers `change_id: null`.

```
{

  "data": {

    "menus": {

      "v": 1,

      "mega": false,

      "top": [

        {"type": "home", "label": "", "ref": null, "url": "", "show_subcategories": false, "children": []},

        {"type": "page", "label": "Guide des tailles", "ref": 9, "url": "", "show_subcategories": false, "children": []}

      ],

      "footer": []

    },

    "version": "b70e4d2a9c15f383",

    "change_id": 90420,

    "automatic": false

  },

  "meta": {"request_id": "a81c3f5e92d07b64", "api_version": "v1"}

}
```

`"menus": null` answers `automatic: true` with the automatic menu in `menus`.

## Undo

`PUT` answers with a `change_id`. `POST /v1/changes/{change_id}/undo` puts back the menu as it was before that call, the automatic menu included. The undo needs `store:write` and an `Idempotency-Key`.

* When the menu changed again since that call, the undo answers `409 menus_changed` and changes nothing. Undo the newer change first.
* A change undone a second time answers `409 already_undone`.
* Menus the merchant saves in Customize are not recorded, so they cannot be undone through the API.
* `GET /v1/changes?entity=store.menus` lists the menu changes, newest first, with `store:read`. `entity_id` is `menus`.
* An installed app's token cannot read or undo changes: `GET /v1/changes` and the undo answer `403 forbidden`. Write answers still carry the `change_id`.

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

  -H "Authorization: Bearer $DZ_KEY" \

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

## Errors

| HTTP | Code                    | Cause                                                                                                                                                                                                                                                                           |
| ---- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400  | `bad_request`           | The body is not a JSON object, `menus` is missing or is neither an object nor `null`, or `version` is not a string.                                                                                                                                                             |
| 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: store:read` or `Missing scope: store:write`, or `API access requires an active Enterprise plan` for a merchant key whose store is not on an active Enterprise plan.                                                                                             |
| 404  | `not_found`             | The store does not exist.                                                                                                                                                                                                                                                       |
| 409  | `write_conflict`        | The `version` you sent no longer matches: the error carries the current `menus` (`null` while the store shows the automatic links) and `version`, so you can merge and retry. The same code without them means another write landed at the same moment: read again, then retry. |
| 409  | `menus_changed`         | Undo only: the menu changed since the change you undo.                                                                                                                                                                                                                          |
| 409  | `already_undone`        | Undo only: the change was already undone.                                                                                                                                                                                                                                       |
| 413  | `payload_too_large`     | The body is over 1 MB.                                                                                                                                                                                                                                                          |
| 422  | `invalid_menus`         | A value was refused. `fields` gives `{path, code}` for it. See the codes below.                                                                                                                                                                                                 |
| 422  | `idempotency_key_reuse` | The same `Idempotency-Key` was used with another body.                                                                                                                                                                                                                          |
| 429  | `rate_limited`          | Too many requests. Wait for `Retry-After`.                                                                                                                                                                                                                                      |
| 500  | `server_error`          | The request failed. Retry with the same `Idempotency-Key`.                                                                                                                                                                                                                      |

`path` starts with `menus.` and points at the refused value, such as `menus.top.0.url` or `menus.footer.1.links.10`; it is `menus` alone for `too_large`. The `code` is one of these.

| `code`             | Cause                                                                                                             |
| ------------------ | ----------------------------------------------------------------------------------------------------------------- |
| `bad_url`          | An empty, refused or too long `url`.                                                                              |
| `invalid`          | A shape error: a list where an object is expected or the reverse, such as `top` that is not an array.             |
| `too_many`         | More than 12 top items, 10 children, 3 groups or 10 links in a group. `path` names the first item over the limit. |
| `top_required`     | `top` is empty. To drop a custom menu, send `"menus": null`.                                                      |
| `unknown_type`     | `type` is not one of the nine types.                                                                              |
| `unknown_key`      | A footer group `key` other than `quick_links`, `categories` or `null`.                                            |
| `unknown_category` | `ref` is not a category of this store.                                                                            |
| `unknown_page`     | `ref` is not a published page of this store.                                                                      |
| `label_required`   | A `url` item without a label, or a footer group without a label and without a `key`.                              |
| `label_too_long`   | A label over 40 characters.                                                                                       |
| `links_required`   | A footer group with no link.                                                                                      |
| `duplicate_type`   | A second `home`, `products`, `categories`, `track`, `contact` or `cart` item in `top`.                            |
| `nested_too_deep`  | `children` inside a sub-link or a footer link, or a `categories` item as a sub-link.                              |
| `too_large`        | The document is over 16 KB.                                                                                       |

```
{

  "error": {

    "code": "invalid_menus",

    "message": "Invalid menu at menus.top.0.url (bad_url); the caps and types are in GET /v1/store/menus.",

    "fields": [{"path": "menus.top.0.url", "code": "bad_url"}]

  },

  "meta": {"request_id": "c27e90b4f13a5d68", "api_version": "v1"}

}
```

## Limits

* **Top menu**: 12 items, each with up to 10 sub-links.
* **Footer**: 3 groups of up to 10 links.
* **Label**: 40 characters. **Url**: 500 bytes. **Document**: 16 KB.
* The limits are the same on every plan.

## MCP tools

The MCP server, version 1.9.0, gives an AI assistant two menu tools with the same rules: `menus_get` returns the whole document with its `version` and caps, `menus_set` writes it back with that `version`, and `change_undo` reverts a write.
