Commerce · REST API
Pages
Read, create, change and delete CMS pages with blocks, images, search-engine fields and a version check.
Overview
Read the CMS pages of the shop (the home page, the legal pages and every other content page) with commerce.read; create, change and delete them with commerce.write.content (edit permission Pages, Pagina's, in Settings > API tokens; a token with it lives at most 30 days). Each endpoint is the MCP tool of the same name, described in Content tools, with the same data, the same change log (channel api) and the same write limit. Every write goes through the page editor's own rules (Website > Pages), is recorded in the shop's content activity log with the token as the causer, and shows in the page's history as "API-token" followed by the token name.
- Drafts are visible to read tokens.
GET /pagesandGET /pages/{id}include drafts and scheduled or expired pages for every token withcommerce.read. Do not give a read token to anyone who may not see unpublished content. - Request size.
POST /pagesandPATCH /pages/{id}accept a body of up to 262144 bytes (256 KiB). Every other write keeps 65536 bytes. - No customer data. Pages hold shop content only.
List pages
/api/v1/pages
Needs commerce.read. Equivalent MCP tool: list_pages.
Query parameters: status (draft, published or all, the default), limit and cursor. Pages by id. The rows have no blocks; use the next endpoint for those.
curl -s -H "Authorization: Bearer $COMMERCE_API_TOKEN" \
"https://<your-shop-domain>/api/v1/pages?status=draft"
Get a page
/api/v1/pages/{id}
Needs commerce.read. Equivalent MCP tool: get_page.
| Field | Type | Meaning |
|---|---|---|
id, title, slug |
Page id, title (at most 255 characters) and the address part. The home page has the slug home. |
|
is_home, url |
Whether it is the home page, and its public address. | |
status |
draft or published. |
|
visibility |
Whether visitors can open it now: draft, scheduled, published or expired. |
|
publish_at, unpublish_at |
string|null | ISO 8601, UTC. |
noindex |
boolean | Keeps the page out of search results. |
updated_at |
The version. PATCH needs it back as expected_updated_at. |
|
meta_title, meta_description, canonical_url, og_image, og_image_url, blocks |
GET /pages/{id} only. og_image is the stored share image path, og_image_url its public URL. blocks is the whole list as stored: each {type, data, image_urls}, where image_urls holds the public URL per image field (null without images). |
The writable fields have the names and forms the write endpoints take, so the answer can be sent back as it is (the image_urls of a block are ignored). With the current version that answers changed: false.
Create a page
/api/v1/pages
Needs commerce.write.content. Equivalent MCP tool: create_page.
| Name | Type | Required | Description |
|---|---|---|---|
title |
string | Required | POST only (optional on PATCH). At most 255 characters. |
slug |
string | Required | POST only (optional on PATCH). Lower-case letters, digits and hyphens, unique, never home. The home page keeps home. |
expected_updated_at |
string | Required | PATCH only. The updated_at you read. When the page was saved after that moment the call is 409 and nothing changes. |
status |
string | Optional | published publishes, draft makes it a draft. Left out keeps it; a new page is a draft. |
publish_at, unpublish_at |
string|null | Optional | ISO 8601 with an offset (a date that does not exist is 422). null clears. The end must be after the start. |
meta_title, meta_description |
string|null | Optional | At most 255 and 320 characters. null or empty: none. |
canonical_url |
string|null | Optional | An https URL or a path of this shop (/pagina/...), only for a copy of another page. null: the page's own address. |
noindex |
boolean | Optional | true adds noindex to the robots meta tag and the X-Robots-Tag header and leaves the page out of the sitemap. |
og_image |
string|null | Optional | The share image: an https URL (downloaded), its current value, or null to remove it. |
blocks |
array | Optional | The whole list in page order, at most 60, each {type, data}. It replaces every block; [] empties the page. |
Not idempotent: a repeat with the same slug is 422 because the slug is taken; a slug that another page takes at the same moment is 409. The answer is the page as GET /pages/{id} returns it, plus changed and changes ({field: {old, new}}; blocks appear as a count and a sha256 fingerprint so the change log does not keep whole texts).
Change a page
/api/v1/pages/{id}
Needs commerce.write.content. Equivalent MCP tool: update_page.
Send expected_updated_at and the fields you want to change; the others keep their value. The input is merged onto the page as it is read under a row lock. Every committed change moves the version, also a second change within the same second, so two writers with the same expected_updated_at never both succeed. When the page was saved after that moment the answer is 409 conflict and nothing changes: read the page again and retry.
curl -sS -X PATCH "https://<your-shop-domain>/api/v1/pages/7" \
-H "Authorization: Bearer $COMMERCE_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"expected_updated_at": "2026-11-01T11:58:00+00:00", "title": "Over ons", "noindex": false, "blocks": [{"type": "heading", "data": {"text": "Over ons", "level": 2}}, {"type": "image", "data": {"path": "https://images.example.com/winkel.jpg", "alt": "Onze winkel"}}]}'
Delete a page
/api/v1/pages/{id}
Needs commerce.write.content. Equivalent MCP tool: delete_page.
No body. Deletes the page with its saved versions and the menu links that point at it. The answer is page_id, deleted, menu_links_removed, changed, changes. The home page cannot be deleted (422); a repeat is 404.
Blocks
blocks is the whole list in page order and replaces every block, so read the page first, change what you need and send the list back. There are 20 block types. Ten work on any page; ten work only on the home page, or on a page that has that type already. The exact fields of each type are in the OpenAPI document (PageBlock, one schema per type). A block that breaks a rule of its type is 422 and names the field.
| type | Where | Main fields |
|---|---|---|
heading |
any page | text (at most 255), level (2, 3 or 4). |
richtext |
any page | html (sanitised), optional aside (reviews). |
faq |
any page | items: at most 20 questions and answers. |
cta |
any page | label (at most 60), url (at most 500), style (primary or secondary). |
usp |
any page | items: at most 6 selling points. |
callout |
any page | heading or text is required; optional note, button_label, button_url. |
image |
any page | path (image), alt (at most 200), optional caption. Width and height are set by the server. |
media-text |
any page | An image beside a heading and rich text: position (left or right), heading or html required, optional path and alt, button_label, button_url. |
feature-grid |
any page | A titled list of at most 8 features with an icon each, and an optional image. |
promo-banners |
any page | Two to four image tiles that link somewhere: ratio (landscape, wide, square), tiles. |
product-row |
home page | A row of products from a collection: collection, sort (featured, newest, sale), limit. |
category-tiles |
home page | Tiles of categories: layout (tiles or list), source (collections or manual), limit, at most 8 entries. |
brand-carousel |
home page | Brand logos; no brands means the most popular ones. At most 50 brands. |
article-teaser |
home page | The latest blog articles: limit. |
locations-teaser |
home page | The shop locations: limit. |
related-products |
home page | Hand-picked products (source manual, at most 12); the other sources only work on a product page. |
product-tabs |
home page | At most 4 tabs of product rows. |
hero-slider |
home page | One to five slides with an image each; autoplay, interval. |
newsletter |
home page | The newsletter sign-up: heading, text, privacy_note. |
goal-tiles |
home page | The health-goal tiles: title, intro. |
- Rich text (
richtext.html,media-text.html) is sanitised as in the editor: scripts, event handlers, inline styles and unsafe links (javascript:) are removed. Other block text is stored as plain text. - References. Collection, brand and product slugs in blocks must exist (
422otherwise), unless the page has them already.
Images
An image field and og_image take either a path the page already uses, which is kept as it is, or an https URL that the shop downloads before the write.
- Public hosts only (no private, loopback or link-local addresses), no redirects, JPEG, PNG or WebP of at most 5 MB each, stored as WebP.
- At most 20 new image URLs per call, 40 MB and 30 seconds of downloading in all. Past the budget the call is
422and names the field. - A GIF, an AVIF or an image above 5 MB is
422and names the field (blocks.2.data.path,og_image). Any other value (anhttpURL, a path of another page) is422. og_imagetakes only anhttpsURL,null(removes the share image) or its current value, never a path a block uses.- A file stored by a call that then fails or rolls back is deleted again.
The home page
- It keeps the slug
home; another slug is422, and no other page may takehome. - It cannot be deleted.
- The ten home-page block types (
product-row,category-tiles,brand-carousel,article-teaser,locations-teaser,related-products,product-tabs,hero-slider,newsletter,goal-tiles) are accepted there, and on another page only when that page has the type already.
Search engines and addresses
noindex: truekeeps the page out of search results, the sitemap andllms.txt. The shop-wide "hide from search engines" setting still wins when it is on.- A page whose
canonical_urlpoints to another address stays in the sitemap for now; setnoindexas well if it must not be listed. - A slug change of a page that is public keeps the old address working with a redirect, as in the editor.
Errors
| Status | error.code | When |
|---|---|---|
403 |
forbidden_scope |
A write without commerce.write.content. |
404 |
not_found |
Unknown page, or a repeated delete. |
409 |
conflict |
PATCH with an expected_updated_at that is no longer current; a slug that another page takes at the same moment. |
422 |
invalid_argument |
A rule that fails, naming the field (blocks.2.data.alt: ...): a slug that is taken or home, a block of a home-page-only type, an unknown collection, brand or product slug, an image that is refused, more than 60 blocks, deleting the home page, or a date that does not exist. |
429 |
too_many_requests |
The write limit. The write did not happen. |