
- MODX 3
- PHP 8.2
- Vue 3


Read-only JSON for an external frontend (Next.js, Nuxt, React, and similar). Returns published PageBuilder content only. Drafts and writes go through the Agent API (Pro) or the resource Sections tab in the manager.
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 | Resource not found. | Unknown resource or API disabled |
| 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.
Compare publishedRevision after a manager publish to decide when to revalidate frontend cache.
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.
Catalog, table, and page queries go through QueryPolicy. Keys sql, php, snippet, and class are rejected. Page size is at most 100. Free api.php stays in place.
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 |