
- MODX 3
- PHP 8.2
- Vue 3


Read-only JSON for an external frontend (Next.js, Nuxt, React). Returns published PageBuilder content only. Drafts and writes go through the Agent API (Pro) or the resource Sections tab.
Entry point:
/assets/components/pagebuilder/api.phpMODX context: web. Methods: GET and OPTIONS (CORS preflight).
System settings, namespace pagebuilder:
| Key | Default | Description |
|---|---|---|
pagebuilder_public_api_enabled | 0 | Turn the API on |
pagebuilder_public_api_key | empty | API key. Empty means no key (dev only) |
pagebuilder_public_api_cors_origins | * | Allowed browser origins |
Clear site cache after you change these keys if MODX does not pick them up immediately.
Details: System settings → Public API.
Example env on the frontend:
PAGEBUILDER_API_URL=https://cms.example.com/assets/components/pagebuilder/api.php
PAGEBUILDER_API_KEY=your-secret-keyWhen pagebuilder_public_api_key is set, send the key as:
X-PageBuilder-Api-Key: <key> (preferred: the key does not land in access logs)api_key=<key>MODX then checks:
publishedRevision > 0)If the API is disabled, protected endpoints return 404. web/health/ping still responds and reports publicApiEnabled.
Same envelope as the manager connector:
{
"success": true,
"message": "",
"object": {}
}| HTTP | Example message | When |
|---|---|---|
| 401 | Invalid API key. | Wrong or missing key |
| 403 | Access denied. | No view policy |
| 404 | Public API is disabled. | pagebuilder_public_api_enabled is off |
| 404 | Resource not found. | Unknown resource |
| 404 | Resource is not published. | Resource exists, but is not published in MODX |
| 404 | Page has no published content. | Resource exists, PageBuilder never published |
| 400 | resource_id or alias is required. | No identifier |
| 400 | Invalid action. | Unknown action= |
web/health/ping Health check. Works when the API is off (publicApiEnabled in the response).
curl -s "https://example.com/assets/components/pagebuilder/api.php?action=web/health/ping"{ "success": true, "object": { "ok": true, "publicApiEnabled": true } }web/page/get Published page for one resource.
| Parameter | Required | Description |
|---|---|---|
resource_id | * | MODX resource ID |
alias | * | Resource alias when resource_id is omitted |
context_key | no | Context for alias, default web |
include | no | Comma list: document, values, html, sections. Default document,values |
section_types | no | Filter types: hero,cta |
api_key | if key set | API key in query |
You need resource_id or alias.
include | Value | In object |
|---|---|
document | Full published JSON: sections, trash, schemaVersion, typeVersion, settings, raw data |
values | Visible sections plus plainText (same idea as [[!PageBuilder? &return_values=1]]) |
html | One HTML string, like the snippet on the site |
sections | Each visible section with its own html from Fenom chunks |
values skips sections hidden by UTM, context, or Pro conditions. document keeps the stored JSON as in published_json.
BASE="https://example.com/assets/components/pagebuilder/api.php"
curl -s -H "X-PageBuilder-Api-Key: ${KEY}" \
"${BASE}?action=web/page/get&alias=about&include=document,values"object (fragment) {
"resourceId": 42,
"publishedRevision": 3,
"resource": {
"id": 42,
"pagetitle": "About",
"alias": "about",
"uri": "about.html",
"context": "web",
"template": 5
},
"document": {
"schemaVersion": 1,
"sections": [
{
"id": "a1b2c3d4",
"type": "hero",
"typeVersion": 1,
"enabled": true,
"data": { "title": "Hello" },
"settings": {}
}
],
"trash": []
},
"values": {
"plainText": "Hello",
"sections": [{ "id": "a1b2c3d4", "type": "hero", "enabled": true, "data": { "title": "Hello" } }]
}
}With include=sections:
{
"id": "a1b2c3d4",
"type": "hero",
"enabled": true,
"data": { "title": "Hello" },
"settings": {},
"html": "<section class=\"pb-section pb-hero\">…</section>",
"renderFailed": false
}renderFailed: true means the chunk did not render (unknown type, Fenom error). Skip the block or show a fallback in your app.
async function getPage(alias) {
const url = new URL(process.env.PAGEBUILDER_API_URL)
url.searchParams.set('action', 'web/page/get')
url.searchParams.set('alias', alias)
url.searchParams.set('include', 'document,values')
const res = await fetch(url, {
headers: { 'X-PageBuilder-Api-Key': process.env.PAGEBUILDER_API_KEY },
next: { revalidate: 60 },
})
const body = await res.json()
if (!body.success) throw new Error(body.message)
return body.object
}/** @var \MODX\Revolution\modX $modx */
$api = $modx->services->get(\PageBuilder\PublicApi\PageBuilderPublicApiService::class);
$api->assertEnabled();
$api->assertApiKey($_SERVER['HTTP_X_PAGEBUILDER_API_KEY'] ?? null);
$page = $api->getPage(
$api->resolveResourceId(0, 'about', 'web'),
['include' => 'document,values'],
);Class PageBuilder\PublicApi\PageBuilderPublicApiService registers in ServiceRegistry.
web/catalog/list Section type metadata for mapping section.type to your components.
| Parameter | Description |
|---|---|
context_key | Catalog context, default web |
curl -s "${BASE}?action=web/catalog/list&context_key=web&api_key=${KEY}"{
"success": true,
"object": {
"items": [
{
"key": "hero",
"version": 1,
"label": "Hero",
"category": "hero",
"categories": ["hero"],
"fields": [
{ "name": "title", "type": "text", "label": "Title", "required": true }
],
"thumbnail": ""
}
]
}
}Cache the list for an hour or until deploy. It changes when you add JSON types or UI types in the CMP.
The type payload includes categories: a list of slugs. category stays for compatibility.
There is no separate tables endpoint. Table rows come from the PageBuilderTableRows snippet or a section datasource.
QueryRequest rejects keys sql, php, snippet, class, classname, and query. This does not apply to api.php query parameters. The limit of 100 applies to a datasource query and to REST GET /pages. web/catalog/list returns every definition with no paging.
Set pagebuilder_public_api_cors_origins to one origin or a comma-separated list:
https://app.example.com,https://staging.example.comPreflight:
curl -s -D - -o /dev/null -X OPTIONS \
"${BASE}?action=web/page/get" \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: GET"Expect Access-Control-Allow-Origin and Access-Control-Allow-Headers: Content-Type, X-PageBuilder-Api-Key.
include | Goal | include |
|---|---|
| SPA with your own components | document or values |
| Keep MODX chunks without rewriting markup | html or sections |
| SEO meta plus blocks | values (plainText, resource fields) |
| One block on a landing page | section_types=hero&include=document |
web/catalog/list once and cache field schemas (or generate TS types from fields).web/page/get by alias or resource_id.object.document.sections with your components, or use object.sections[].html / object.html for an SSR hybrid.publishedRevision changes.| Event | When |
|---|---|
pbOnGetValues | include contains values |
pbOnBeforeGetList | web/catalog/list |
Plugins can adjust plainText or hide types the same way as on the public site.
| Public API | Agent API (Pro) | |
|---|---|---|
| URL | api.php | connector.php |
| Context | web | mgr |
| Auth | API key (optional) | Manager session + modAuth |
| Data | published only | draft, schemas, published snapshot |
| Write | no | mgr/api/page/apply |
| Feature | Free | Pro |
|---|---|---|
| Public API read (published) | yes | yes |
| Agent API write | no | yes |
| Pro-only section types in output | yes, if published on the page | yes |