Widget API
Widgets are configurable UI components rendered into storefront areas. A widget is stored as two things:
- a widget instance (
widget_instance) — the type, name, status, theme and settings; - zero or more placements (
widget_placement) — one row per(route, area)cell, each with its ownsort_order.
That separation is the main change to be aware of: area, route and sort_order are no longer columns on the widget itself. The primary key column is widget_instance_id, and the settings column is settings (not setting).
Endpoints
Create a Widget
Creates a widget instance and its placements.
| Field Name | Field Type | Required | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| type | string | No | ||||||||||||
| name | string | Yes | ||||||||||||
| status | string or integer (0, 1, '0', '1') | Yes | ||||||||||||
| settings | object | No | ||||||||||||
| placements | array of object | No | ||||||||||||
| ||||||||||||||
| route | array of string | No | ||||||||||||
| area | array of string | No | ||||||||||||
| sort_order | string or integer | No | ||||||||||||
- cURL
- JavaScript
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
--data-raw '<JSON DATA>'
https://<your domain>/api/widgets
fetch('https://<your domain>/api/widgets', {
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer <admin JWT token>'
},
body: <JSON DATA>
})
.then(response => response.json())
.then(data => {
if(data.error) {
// Handle the error
} else {
// Handle the data
}
})
.catch(error => {
// Handle the error
});
{
"data": {
"widget_instance_id": 5,
"uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"type": "collection_products",
"name": "Featured Products",
"status": 1,
"theme": "eve",
"settings": {
"collection": "summer-sale",
"count": 4
},
"links": [
{
"rel": "widgetGrid",
"href": "/admin/widgets",
"action": "GET",
"types": ["text/xml"]
},
{
"rel": "edit",
"href": "/admin/widgets/edit/a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"action": "GET",
"types": ["text/xml"]
}
]
}
}
A create payload using the preferred placement shape:
{
"type": "collection_products",
"name": "Featured Products",
"status": 1,
"settings": {
"collection": "summer-sale",
"count": 4
},
"placements": [
{ "route": "homepage", "area": "content", "sort_order": 10 },
{ "route": "categoryView", "area": "beforeFooter", "sort_order": 20 }
]
}
Placement entries missing route or area are skipped silently. If you send neither placements nor route/area, the widget is created with zero placements and renders nowhere.
Update a Widget
Updates the instance and, when the payload touches placement fields, rebuilds its placements.
| Field Name | Field Type | Required | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| name | string | No | ||||||||||||
| status | string or integer (0, 1, '0', '1') | Yes | ||||||||||||
| settings | object | No | ||||||||||||
| placements | array of object | No | ||||||||||||
| ||||||||||||||
| route | array of string | No | ||||||||||||
| area | array of string | No | ||||||||||||
| sort_order | string or integer | No | ||||||||||||
- cURL
- JavaScript
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
--data-raw '<JSON DATA>'
https://<your domain>/api/widgets/{id}
fetch('https://<your domain>/api/widgets/{id}', {
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer <admin JWT token>'
},
body: <JSON DATA>
})
.then(response => response.json())
.then(data => {
if(data.error) {
// Handle the error
} else {
// Handle the data
}
})
.catch(error => {
// Handle the error
});
{
"data": {
"widget_instance_id": 5,
"uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Featured Products Updated",
"status": 1,
"links": [
{
"rel": "widgetGrid",
"href": "/admin/widgets",
"action": "GET",
"types": ["text/xml"]
},
{
"rel": "edit",
"href": "/admin/widgets/edit/a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"action": "GET",
"types": ["text/xml"]
}
]
}
}
Placement replacement rules:
| Payload | Effect on placements |
|---|---|
placements present | Route-level placements are deleted and recreated from the list. Entity-scoped placements (the ones the page builder owns) are left untouched |
any of route, area, sort_order present | All placements are deleted and recreated from the cross-product of route x area |
| none of the above | Placements are left untouched |
Delete a Widget
Permanently removes a widget instance. Its placements cascade.
- cURL
- JavaScript
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
https://<your domain>/api/widgets/{id}
fetch('https://<your domain>/api/widgets/{id}', {
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer <admin JWT token>'
}
})
.then(response => response.json())
.then(data => {
if(data.error) {
// Handle the error
} else {
// Handle the data
}
})
.catch(error => {
// Handle the error
});
{
"data": {
"widget_instance_id": 5,
"uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}
Notes
{id} is the instance uuid
Every /api/widgets/{id} endpoint resolves the widget by widget_instance.uuid. A well-formed but unknown uuid returns Requested widget not found on PATCH and Invalid widget id on DELETE. Passing the integer widget_instance_id is not a "not found" case at all: uuid is a Postgres uuid column, so the comparison raises invalid input syntax for type uuid and both endpoints surface it as a 500.
theme is stamped on create only
On create, the widget's theme is stamped from the active theme after validation, so new widgets land in the current theme's bucket.
On update it is not re-stamped. updateWidget passes the payload straight to .given(data), so a theme value in the PATCH body is written through to the row and can move the widget into another theme's bucket — where the storefront's theme filter will stop matching it. Do not send theme on update.
settings is schema-validated
When the widget type was registered with a JSON Schema, settings is compiled and validated against it. A mismatch fails the whole request with:
{
"error": {
"status": 500,
"message": "Widget settings failed schema validation: [{\"instancePath\":\"/count\",\"message\":\"must be integer\"}]"
}
}
Widget types registered without a schema skip this step. Note that settings are stored as sent — nothing parses a JSON string into an array or object on your behalf, so send real arrays and objects.