# Store pages

Store pages are the merchant's text pages, such as About us, Delivery or Returns. Each page is written in Markdown. A published page lives at `/pages/{slug}` on the store and is listed in the store's `sitemap.xml` and `llms.txt`. A draft answers `404` on the store. These five endpoints list, read, create, change and delete the pages.

The merchant edits the same pages in the dashboard, in Customize, row **Pages**. See [Store pages](https://dzbuild.com/docs/storefront/pages) in the merchant docs.

## Before you start

* Reads need `store:read` and writes need `store:write`. Merchant keys carry both.
* `POST`, `PATCH` and `DELETE` need an `Idempotency-Key`. See [Idempotency](https://dzbuild.com/api-docs/idempotency.md).
* The API cannot upload images. An image in `body_md` is a link to a picture that is already online, written as `![text](https://...)`.

## The page object

| Field             | Type           | Notes                                                                                                                                                                                                                   |
| ----------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`              | int            | The page id.                                                                                                                                                                                                            |
| `slug`            | string         | The end of the page address, after `/pages/`. 2 to 80 lowercase Latin letters, digits or dashes, starting and ending with a letter or a digit. Unique in the store.                                                     |
| `title`           | string         | 1 to 120 characters.                                                                                                                                                                                                    |
| `body_md`         | string         | The text in Markdown, up to 65,536 bytes. See [How the body is rendered](#how-the-body-is-rendered).                                                                                                                    |
| `seo_title`       | string or null | The title search engines show, up to 70 characters. `null` uses `title`.                                                                                                                                                |
| `seo_description` | string or null | The description search engines show, up to 160 characters. `null` uses the first lines of the text.                                                                                                                     |
| `status`          | string         | `draft` or `published`.                                                                                                                                                                                                 |
| `translations`    | object or null | The texts of the store's second language, keyed by language code, such as `{"fr": {"title": "...", "body_md": "...", "seo_title": null, "seo_description": null}}`, or `null`. See [Second language](#second-language). |
| `url`             | string         | The full address of the page on the store.                                                                                                                                                                              |
| `created_at`      | string         | `YYYY-MM-DD HH:MM:SS`, server time.                                                                                                                                                                                     |
| `updated_at`      | string         | `YYYY-MM-DD HH:MM:SS`, server time.                                                                                                                                                                                     |

HTML tags in `title`, `seo_title` and `seo_description` are removed, and text over the limit is cut.

### How the body is rendered

* Markdown is rendered in safe mode: raw HTML is escaped and shows as text.
* A single line break in the text is kept as a line break.
* A link to the store itself, such as `/products`, `/category/12` or `/pages/delivery`, opens in the same tab. A link to another site opens in a new tab, and so does a link that starts with `//`.
* A YouTube link alone on its line shows the video.

### Second language

`translations` is written only while the Multi-Language add-on is active on the store. Without it, the field is ignored and the stored value is kept. The key is the store's second language: `fr` for an Arabic store, `ar` for a French one. A buyer who browses the store in that language sees those texts, and an empty field falls back to the main field.

## `GET /v1/store/pages`

All the store's pages, drafts included, ordered by title. The items are summaries without the text: read one page for its `body_md`. The list is not paginated: a store has at most 25 pages.

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

### Request

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

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

### Response 200

Each item carries `id`, `slug`, `title`, `status`, `url` and `updated_at`. `cap` is the number of pages the store's plan allows. `max_pages` is the hard limit for every plan.

```
{

  "data": {

    "items": [

      {

        "id": 7,

        "slug": "retours",

        "title": "Retours",

        "status": "published",

        "url": "https://my-store.dzbuild.app/pages/retours",

        "updated_at": "2026-11-06 10:12:40"

      }

    ],

    "cap": 25,

    "max_pages": 25

  },

  "meta": {"request_id": "3f1c9a7b20d4e865", "api_version": "v1"}

}
```

## `GET /v1/store/pages/{id}`

One page.

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

### Request

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

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

### Response 200

The answer is the page object. A page that is not in the store answers `404 page_not_found`.

## `POST /v1/store/pages`

Creates a page.

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

### Body

| Field             | Type                   | Required | Notes                                                                                                                                                                                                                                                                        |
| ----------------- | ---------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `title`           | string                 | yes      | 1 to 120 characters.                                                                                                                                                                                                                                                         |
| `slug`            | string                 | no       | Made from the title when you leave it out. When that gives no usable slug (a title with no Latin letters or digits such as an Arabic one, a reserved word, or a slug another page uses), the page gets `page-N`. A slug you send is lowercased, then must be valid and free. |
| `body_md`         | string                 | no       | Markdown, up to 65,536 bytes.                                                                                                                                                                                                                                                |
| `seo_title`       | string or null         | no       | Up to 70 characters.                                                                                                                                                                                                                                                         |
| `seo_description` | string or null         | no       | Up to 160 characters.                                                                                                                                                                                                                                                        |
| `status`          | `draft` or `published` | no       | Defaults to `draft`. Publish only when the merchant asked.                                                                                                                                                                                                                   |
| `translations`    | object or null         | no       | See [Second language](#second-language).                                                                                                                                                                                                                                     |

A `slug` must not be a word the store already uses for its own addresses, such as `products`, `product`, `cart`, `checkout`, `category`, `track-order`, `landing`, `pages`, `sitemap`, `ar` or `fr`. Such a slug answers `422 invalid_page`.

### Request

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

  -H "Authorization: Bearer $DZ_KEY" \

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

  -H "Idempotency-Key: page-create-retours-1" \

  -d '{"title": "Retours", "slug": "retours", "body_md": "## Comment retourner un article\n\nÉcrivez-nous sur WhatsApp avec votre numéro de commande.", "status": "published"}'
```

### Response 201

```
{

  "data": {

    "page": {

      "id": 7,

      "slug": "retours",

      "title": "Retours",

      "body_md": "## Comment retourner un article\n\nÉcrivez-nous sur WhatsApp avec votre numéro de commande.",

      "seo_title": null,

      "seo_description": null,

      "status": "published",

      "translations": null,

      "url": "https://my-store.dzbuild.app/pages/retours",

      "created_at": "2026-11-06 10:12:40",

      "updated_at": "2026-11-06 10:12:40"

    },

    "change_id": 90310

  },

  "meta": {"request_id": "8b2e04d1c9a7f356", "api_version": "v1"}

}
```

## `PATCH /v1/store/pages/{id}`

Changes only the fields you send. The body takes the fields of `POST`, all optional. A body with none of them answers `422 no_changes`, and a call that changes nothing answers `change_id: null`.

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

* A new `slug` changes the page address. The old address keeps answering with a `301` redirect to the new one, so shared links keep working. Setting the old slug back is allowed. Leaving `slug` out keeps the address, even when the title changes.
* `"status": "draft"` takes the page off the store without deleting it.

### Request

```
curl -X PATCH 'https://api.dzbuild.app/v1/store/pages/7' \

  -H "Authorization: Bearer $DZ_KEY" \

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

  -H "Idempotency-Key: page-7-seo-1" \

  -d '{"seo_title": "Retours et échanges", "seo_description": "Comment retourner un article commandé sur notre boutique."}'
```

### Response 200

The answer has the same fields as the `POST` answer: `page`, the page after the change, and `change_id`.

## `DELETE /v1/store/pages/{id}`

Deletes a page. Its address on the store stops working at once.

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

### Request

```
curl -X DELETE 'https://api.dzbuild.app/v1/store/pages/7' \

  -H "Authorization: Bearer $DZ_KEY" \

  -H "Idempotency-Key: page-7-delete-1"
```

### Response 200

```
{

  "data": {

    "deleted": true,

    "id": 7,

    "change_id": 90312

  },

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

}
```

## Undo

Every write answers with a `change_id`. `POST /v1/changes/{change_id}/undo` reverts that change. The undo needs `store:write` and an `Idempotency-Key`.

* Undoing a `PATCH` puts back the page as it was before that call. When the page was deleted since, the undo answers `422 restore_target_missing`.
* Undoing a `DELETE` creates the page again under its old id and slug.
* A create cannot be undone: the undo answers `422 nothing_to_restore`. Delete the page instead.
* When another page took the id or the slug of a deleted page since, or the store already holds 25 pages, the undo of the delete answers `409 page_restore_conflict`. Rename or delete that page, then undo again.
* A change undone a second time answers `409 already_undone`.
* Changes the merchant saves in Customize are not recorded, so they cannot be undone through the API.
* `GET /v1/changes?entity=store.page` lists the page changes, newest first, with `store:read`. `entity_id` is the page id.
* An installed app's token cannot read or undo changes: `GET /v1/changes` and the undo answer `403 forbidden` (`Apps cannot use this endpoint`). Write answers still carry the `change_id`.

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

  -H "Authorization: Bearer $DZ_KEY" \

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

```
{

  "data": {

    "undone": true,

    "change_id": 90312,

    "entity": "store.page",

    "undo_change_id": 90315

  },

  "meta": {"request_id": "71d5e0a2c8b94f36", "api_version": "v1"}

}
```

## Errors

| HTTP | Code                     | Cause                                                                                                                                                                                                                                                                                              |
| ---- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400  | `bad_request`            | The body is not a JSON object, the page id in the path is not a positive number, 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: 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.                                                                                                                |
| 403  | `plan_required`          | A create would go over the number of pages the plan allows. The error carries `plan` and `cap`.                                                                                                                                                                                                    |
| 404  | `page_not_found`         | No page with this id in the store.                                                                                                                                                                                                                                                                 |
| 409  | `slug_taken`             | Another page of the store uses this slug. `fields` is `[{"path": "slug", "code": "taken"}]`.                                                                                                                                                                                                       |
| 409  | `write_conflict`         | Another write landed at the same moment. Read the page, then retry with a new `Idempotency-Key`.                                                                                                                                                                                                   |
| 409  | `already_undone`         | Undo only: the change was already undone.                                                                                                                                                                                                                                                          |
| 409  | `page_restore_conflict`  | Undo of a delete only: another page now uses the page's id or slug, or the store already holds 25 pages.                                                                                                                                                                                           |
| 413  | `payload_too_large`      | The body is over 1 MB.                                                                                                                                                                                                                                                                             |
| 422  | `invalid_page`           | A value was refused: a missing or empty title, a field that is not text, a malformed or reserved slug, a `status` other than `draft` and `published`, `translations` that is not an object, or a `body_md` over 65,536 bytes. `fields` gives `{path, code}` for the refused field, such as `slug`. |
| 422  | `limit_reached`          | The store already has 25 pages. The error carries `limit`.                                                                                                                                                                                                                                         |
| 422  | `no_changes`             | A `PATCH` with none of the page fields.                                                                                                                                                                                                                                                            |
| 422  | `nothing_to_restore`     | Undo only: the change created the page.                                                                                                                                                                                                                                                            |
| 422  | `restore_target_missing` | Undo of a `PATCH` only: the page was deleted since.                                                                                                                                                                                                                                                |
| 422  | `idempotency_key_reuse`  | The same `Idempotency-Key` was used with another body.                                                                                                                                                                                                                                             |
| 429  | `rate_limited`           | Too many requests, including more than 30 page writes a minute for the store. Wait for `Retry-After`.                                                                                                                                                                                              |
| 429  | `too_many_concurrent`    | More than 5 page writes running at once for the store. Retry in a few seconds.                                                                                                                                                                                                                     |
| 500  | `server_error`           | The request failed. Retry with the same `Idempotency-Key`.                                                                                                                                                                                                                                         |

An `invalid_page` answer looks like this.

```
{

  "error": {

    "code": "invalid_page",

    "message": "slug: 2 to 80 lowercase latin letters, digits or dashes, not a store route",

    "fields": [{"path": "slug", "code": "invalid"}]

  },

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

}
```

## Limits

* **25 pages** per store (`max_pages`), drafts included.
* **Plan cap** (`cap`): 3 pages on Free or an expired plan, 25 from Pro. A store above its cap after a downgrade keeps its pages and can still read, change and delete them. A create answers `403 plan_required` until the store is under its cap.
* **Body**: `body_md` up to 65,536 bytes, and the request up to 1 MB.
* **Writes**: 30 a minute and 5 at once per store, on top of the store's per-minute limit. See [Rate limits](https://dzbuild.com/api-docs/rate-limits.md).

## MCP tools

The MCP server, version 1.8.0, gives an AI assistant five page tools with the same rules: `pages_list`, `page_get`, `page_create`, `page_update` and `page_delete`. The `page_delete` tool tells the assistant to confirm with the merchant first.
