Skip to main content

Page Builder API

Overview

The page builder never edits storefront widgets directly. It records every action as an operation on a changeset, and a changeset reaches shoppers in one of two ways: by being published (applied transactionally to the source tables) or by being attached to a rollout plan (applied as an in-memory overlay for a scheduled window). The concepts are explained in Page Builder; this page is the endpoint reference.

There are ten endpoints, all access: private — every one of them requires an admin access token (see Authentication).

{id} here is the integer primary key, not a uuid

This is the exception to EverShop's usual convention. Every {id} on this API is parsed with Number(...) and validated with Number.isInteger(...), so it must be a changeset.changeset_id or a rollout_plan.rollout_plan_id. Passing a uuid returns 400 Invalid changeset id / 400 Invalid rollout plan id.

The ?session= query parameter on the editor page (/admin/page-builder/edit/:routeId) does take a rollout_plan.uuid — do not confuse the two.

Every endpoint sends its own response and returns errors in the standard envelope:

{
"error": {
"status": 400,
"message": "change_order must be a non-negative integer"
}
}

Changeset endpoints

Create A Changeset

Creates an empty changeset owned by the authenticated admin and mints a random v4 UUID token for previewing it at <storefront-url>?changeset=<token>. Responds 201.

The editor does not use this endpoint

The page builder resolves its session with the server-side getOrCreateDraftChangeset service, which is what enforces "one open draft per (admin, theme)" and stamps the draft's theme column. This endpoint creates a changeset with theme = NULL, so a changeset created here previews correctly only on a store running without a custom theme — otherwise the preview theme guard rejects it with 409/302. Use it for scripted or integration scenarios, not to emulate the editor.

Request Schema (application/json)
Field NameField TypeRequired
namestringYes
POST/api/page-builder/changesets
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
--data-raw '<JSON DATA>'
https://<your domain>/api/page-builder/changesets
Sample Of Response
{
"data": {
"changeset_id": 42,
"uuid": "9a1c1c1e-0d2f-4a7f-9b64-3d0a8b4f1c22",
"name": "Spring campaign",
"route_cursors": {},
"token": "6f2a0f9d-63cf-4e1a-9d5a-7b1f3a2c8e10",
"published_at": null,
"created_by": 1,
"theme": null,
"created_at": "2026-08-12 09:41:03",
"updated_at": "2026-08-12 09:41:03"
}
}

Add An Operation To A Changeset

Records one insert / update / delete against widget_instance or widget_placement. Responds 201 with the persisted changeset_operation row.

The operation type is inferred from the payloads: (null, set) is an INSERT, (set, set) an UPDATE, (set, null) a DELETE. Sending both as null is rejected.

Server-side behaviour you cannot override:

BehaviourDetail
change_order is reallocatedThe field is required and validated as a non-negative integer, then discarded. The server allocates MAX(change_order) + 1 under a SELECT ... FOR UPDATE row lock on the changeset.
Redo stack truncationOperations on the same route with a change_order above that route's cursor are deleted first. Other routes are untouched.
Theme stampingOn an INSERT, the changeset's theme is written onto new_payload.theme, overriding any client-supplied value. On an UPDATE or DELETE, a target row belonging to another theme is rejected with 400 theme scope violation.
Ownership403 when the changeset is another admin's pb-draft-* row. Rollout-backed changesets are deliberately shared between admins.
Immutability400 Cannot add operations to a published changeset.
Request Schema (application/json)
Field NameField TypeRequired
routestringYes
entity_urnstringYes
old_payloadobject or nullNo
new_payloadobject or nullNo
change_orderintegerYes
POST/api/page-builder/changesets/42/operations
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
--data-raw '<JSON DATA>'
https://<your domain>/api/page-builder/changesets/42/operations
Sample Of Response
{
"data": {
"changeset_operation_id": 118,
"uuid": "b7d2a4e6-4c11-4a3f-8f21-91d0c3a5e004",
"changeset_id": 42,
"route": "homepage",
"entity_urn": "urn:evershop:cms:widget_placement:1f0e9b2a-77c3-4a15-9f0c-2a1b6d8e4c31",
"old_payload": null,
"new_payload": {
"uuid": "1f0e9b2a-77c3-4a15-9f0c-2a1b6d8e4c31",
"area": "content",
"route": "homepage",
"theme": null,
"sort_order": 15,
"entity_urn": null,
"widget_instance_uuid": "5c9d1b77-8a63-4a2b-b0f1-0f4c8e2d7a99"
},
"change_order": 7,
"created_at": "2026-08-12 09:44:19"
}
}

Publish A Changeset

Applies every operation inside the changeset's cursor window to the source tables, in change_order ascending order, inside a single transaction, and stamps published_at. Responds 200 with the updated changeset row.

Failures roll the whole transaction back and leave the changeset unpublished. Two side effects to plan for:

  • The changeset becomes immutable — further operation adds, undo/redo and discards are rejected.
  • Any rollout_plan rows referencing the changeset are deleted in the same transaction. Do not hold a rollout plan ID across a publish.

Re-publishing returns 400 Changeset is already published.

Request Schema (application/json)
No request body required
POST/api/page-builder/changesets/42/publish
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
--data-raw '<JSON DATA>'
https://<your domain>/api/page-builder/changesets/42/publish
Sample Of Response
{
"data": {
"changeset_id": 42,
"uuid": "9a1c1c1e-0d2f-4a7f-9b64-3d0a8b4f1c22",
"name": "Spring campaign",
"route_cursors": {
"homepage": 7
},
"token": "6f2a0f9d-63cf-4e1a-9d5a-7b1f3a2c8e10",
"published_at": "2026-08-12 10:02:55",
"created_by": 1,
"theme": null,
"created_at": "2026-08-12 09:41:03",
"updated_at": "2026-08-12 10:02:55"
}
}

Move The Current Change (Undo / Redo)

Moves one route's cursor in changeset.route_cursors. Undo walks to the largest change_order strictly below the current one on that route; redo walks to the smallest one strictly above. Neither touches any other route's cursor. Responds 200.

When the changeset is attached to a rollout plan, undo has a floor at rollout_plan.route_cursors[route] — that state is what the storefront is currently rendering, so the editor is not allowed to drop below it. Reverting past the floor requires a discard or a rollout cancellation.

Redo with nothing ahead is a silent no-op that returns the unchanged cursor.

Request Schema (application/json)
Field NameField TypeRequired
directionstring (undo, redo)Yes
routestringYes
POST/api/page-builder/changesets/42/move-current
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
--data-raw '<JSON DATA>'
https://<your domain>/api/page-builder/changesets/42/move-current
Sample Of Response
{
"data": {
"direction": "undo",
"route": "homepage",
"routeCursors": {
"homepage": 6
},
"currentChangeOrder": 6,
"canUndo": true,
"canRedo": true
}
}

Discard A Changeset

Two different semantics depending on whether the changeset is attached to a rollout plan. Read data.rollout and data.changesetDeleted — do not assume the changeset is gone. Responds 200.

ChangesetScopeEffect
Draft (no rollout plan)No ?routeDeletes every operation and the changeset row. changesetDeleted: true.
Draft (no rollout plan)?route=homepageDeletes operations on that route and drops its route_cursors entry. If no operations remain anywhere, the changeset row is deleted too.
Rollout-backedNo ?routeRevert to snapshot. Restores changeset.route_cursors to the rollout's saved cursors and deletes only the operations above them, across the union of routes in either map. rollout: true, changesetDeleted: false.
Rollout-backed?route=homepageSame revert, restricted to that route.

A published changeset cannot be discarded — it is part of the audit trail (400 Cannot discard a published changeset).

Request Schema (application/json)
No request body required
POST/api/page-builder/changesets/42/discard?route=homepage
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
--data-raw '<JSON DATA>'
https://<your domain>/api/page-builder/changesets/42/discard?route=homepage
Sample Of Response
{
"data": {
"discarded": true,
"mode": "route",
"route": "homepage",
"rollout": true,
"changesetDeleted": false
}
}

Rollout plan endpoints

A rollout plan makes a changeset visible for a scheduled window without writing to the source tables. There is no scheduler: loadActiveOps evaluates start_time <= NOW() AND (end_time IS NULL OR end_time > NOW()) on every storefront request, so creating, editing or cancelling a plan takes effect on the very next request.

Create A Rollout Plan

Responds 201. The plan snapshots the changeset's route_cursors and inherits its theme — from then on the storefront renders the snapshot, not the editor's live cursors, until Sync is called.

Rejections to expect:

  • 400 when the proposed window overlaps an existing active-or-upcoming plan. The message lists the conflicting plan names. The overlap check is global for v1.
  • 400 Cannot schedule a rollout for a published changeset.
  • 400 end_time must be after start_time.
  • 404 when the changeset does not exist.

On success, a changeset still named pb-draft-<userId> is renamed to the plan's name, so the admin's draft bucket is freed and their next editor visit mints a fresh draft.

Request Schema (application/json)
Field NameField TypeRequired
namestringYes
changeset_idintegerYes
start_timestringYes
end_timestring or nullNo
POST/api/page-builder/rollout-plans
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
--data-raw '<JSON DATA>'
https://<your domain>/api/page-builder/rollout-plans
Sample Of Response
{
"data": {
"rollout_plan_id": 8,
"uuid": "3c7f9a41-2b6d-4c8e-9a10-5f2b7d4e1c93",
"name": "Black Friday banner",
"changeset_id": 42,
"route_cursors": {
"homepage": 7
},
"theme": null,
"start_time": "2026-11-27 00:00:00+00",
"end_time": "2026-12-01 00:00:00+00",
"created_at": "2026-08-12 10:15:00",
"updated_at": "2026-08-12 10:15:00"
}
}

List Rollout Plans

Responds 200 with plans plus a summary of the changeset behind each one. Optional ?status filter, defaulting to all. An invalid value returns 400.

statusFilter
activestart_time <= NOW() AND (end_time IS NULL OR end_time > NOW())
upcomingstart_time > NOW()
pastend_time IS NOT NULL AND end_time <= NOW()
allNo time filter (default)

Sort order is indefinite plans first (end_time IS NULL), then start_time descending, then rollout_plan_id descending. Each row carries indefinite: true when it has no end time. Keys in this response are camelCase, unlike the other endpoints on this page, which return raw snake_case database rows.

Request Schema (application/json)
No request body required
GET/api/page-builder/rollout-plans?status=active
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
https://<your domain>/api/page-builder/rollout-plans?status=active
Sample Of Response
{
"data": {
"status": "active",
"rolloutPlans": [
{
"rolloutPlanId": 8,
"uuid": "3c7f9a41-2b6d-4c8e-9a10-5f2b7d4e1c93",
"name": "Black Friday banner",
"changesetId": 42,
"startTime": "2026-11-27T00:00:00.000Z",
"endTime": null,
"createdAt": "2026-08-12T10:15:00.000Z",
"updatedAt": "2026-08-12T10:15:00.000Z",
"indefinite": true,
"changeset": {
"name": "Black Friday banner",
"uuid": "9a1c1c1e-0d2f-4a7f-9b64-3d0a8b4f1c22",
"publishedAt": null,
"operationCount": 7
}
}
]
}
}

Update A Rollout Plan's Schedule

Edits the plan's name, start_time and end_time. Responds 200 with the updated row. It runs the same overlap check as create, excluding the plan being edited, so an in-place edit can keep its own window.

This endpoint does not touch route_cursors or changeset_id — moving the live content forward is Sync's job.

Request Schema (application/json)
Field NameField TypeRequired
namestringYes
start_timestringYes
end_timestring or nullNo
PATCH/api/page-builder/rollout-plans/8
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
--data-raw '<JSON DATA>'
https://<your domain>/api/page-builder/rollout-plans/8
Sample Of Response
{
"data": {
"rollout_plan_id": 8,
"uuid": "3c7f9a41-2b6d-4c8e-9a10-5f2b7d4e1c93",
"name": "Black Friday banner (extended)",
"changeset_id": 42,
"route_cursors": {
"homepage": 7
},
"theme": null,
"start_time": "2026-11-25 00:00:00+00",
"end_time": "2026-12-05 00:00:00+00",
"created_at": "2026-08-12 10:15:00",
"updated_at": "2026-08-12 11:02:31"
}
}

Sync A Rollout Plan

The "Save" action of rollout-edit mode. Copies changeset.route_cursors into rollout_plan.route_cursors, promoting whatever the editor currently shows to what the storefront renders. Responds 200.

Idempotent — when the two maps already match, the update rewrites the same JSONB. Syncing a plan whose changeset has been published returns 400 Cannot sync a published changeset.

Request Schema (application/json)
No request body required
POST/api/page-builder/rollout-plans/8/sync
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
--data-raw '<JSON DATA>'
https://<your domain>/api/page-builder/rollout-plans/8/sync
Sample Of Response
{
"data": {
"synced": true,
"routeCursors": {
"homepage": 11,
"cart": 3
}
}
}

Cancel A Rollout Plan

Deletes the plan. The underlying changeset is preserved — it stays queryable and can be scheduled again. Responds 200.

An active rollout stops applying on the next storefront request; a not-yet-started one simply never starts; an already-ended one is just cleaned up.

Request Schema (application/json)
No request body required
DELETE/api/page-builder/rollout-plans/8
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
https://<your domain>/api/page-builder/rollout-plans/8
Sample Of Response
{
"data": {
"cancelled": true
}
}

Notes

Preview a changeset without publishing it

Append the changeset's token to any storefront URL:

curl "https://<your domain>/?changeset=6f2a0f9d-63cf-4e1a-9d5a-7b1f3a2c8e10"

Possession of the token is the authorization. The storefront request runs under the storefront session, not the admin one, so there is no session check — any request presenting a valid token gets the overlay. Treat tokens as secrets and strip the changeset parameter from anything you log or share.

The preview is also theme-guarded: a token whose changeset belongs to a different theme than the active one is rejected with 409 (JSON clients) or 302 (browsers).

Reading changesets with GraphQL

The admin GraphQL schema exposes Query.changeset, Query.changesets, Query.rolloutPlan, Query.rolloutPlans and Query.activeRolloutPlans, plus per-route operation counts (Changeset.operationCountForRoute, Changeset.operationCountsByRoute). Use those for reads — the REST surface above is write-oriented, and only GET /api/page-builder/rollout-plans returns a collection.

Further reading

The model behind these endpoints — draft scoping, the overlay engine, the two-cursor system, per-route undo/redo — is documented in Page Builder.