Internal Content API
Purpose
The Internal Content API is a secure CMS API for trusted AI and operator tools. It lets those tools inspect CMS content contracts, create draft-first content, replace specific page-owned slots on existing draft pages, and run explicit publish operations through structured JSON without logging into, scraping, or automating the browser admin UI.
Phase 1 is implemented as a token-protected, JSON-only, non-public API for read-only content discovery plus draft page creation through validated content plans. Phase 2A adds safe foundations for navigation menus, Shared Slots, and explicit page slot Shared Slot assignment. Phase 2B adds controlled draft-only replacement for page-owned slot content on existing pages. Publish endpoints are explicit and require content.publish; content apply remains draft-first and does not publish. The API remains intentionally narrow: no remote fetch, no broad page delete through content apply, no replacement of Shared Slot-backed slots, and no Shared Slot cascade publishing.
Product Positioning
The Internal Content API is:
- an internal/operator CMS API
- token-protected
- not public
- not a headless CMS delivery API
- not a replacement for admin permissions
- not an import/export replacement
- not an AI vendor integration
CMS core should own this API because it operates on core content concepts: sites, pages, layouts, slots, blocks, translations, navigation, and shared slots. AI or operator tools may call the API, but CMS core should not embed OpenAI, LLM, crawler, or vendor-specific integration logic.
Route Prefix
The canonical prefix is:
/webadmin/api
This keeps the API inside the CMS admin boundary while using a concise, familiar API segment. Resource-style endpoints should live directly under this prefix, such as /webadmin/api/pages and /webadmin/api/blocks.
API discovery starts at:
GET /webadmin/api
Unauthenticated callers receive only public-safe bootstrap JSON. Authenticated callers receive safe product version metadata plus links to OpenAPI, the AI guide, content contract, examples, content validate/apply, pages, navigation, and Shared Slots. External AI/operator tools should start from this live discovery response instead of reading the CMS repository or local package docs.
Plan-based content operations use:
POST /webadmin/api/content/validate
POST /webadmin/api/content/apply
Route choices to avoid:
/webadmin/internal-api, because it is unnecessarily verbose/webadmin/api/content-plans/..., becausecontent-plansis too technical and narrow for the URL contract- placing every resource under
/webadmin/api/content/..., because resource APIs should stay clear and direct /admin, because CMS must not assume the host product's/adminpath is CMS-owned/cms, because/cmsremains reserved for static CMS assets only
Authentication
The API uses Bearer token authentication:
Authorization: Bearer <token>
CMS API tokens are created by a CMS super admin from System -> API Tokens. The CMS stores only a SHA-256 hash plus a safe preview in the cms_api_tokens database table. The plain token is shown once immediately after creation and is never shown again.
Super admins can revoke a token to immediately disable API access while keeping the audit row visible, or delete a token to permanently remove the token record from the list. Deleting an active token also immediately disables API access because the authenticator can no longer find a matching stored hash.
Local AI and operator tools should store the generated token in a trusted operator secret store.
Use the Internal Content API base URL in local operator configuration:
WEBBLOCKS_CMS_API_URL=https://example.com/webadmin/api
WEBBLOCKS_CMS_API_TOKEN=...
The CMS runtime does not require WEBBLOCKS_CMS_INTERNAL_API_TOKEN.
Authentication rules:
- missing, wrong, or revoked tokens return JSON
401 - revoked tokens stop working immediately
- tokens must never be printed in logs, diagnostics, support reports, tests, or documentation examples
- token comparison must use a constant-time comparison
- successful API requests update the token's
last_used_atandlast_used_ip - successful API requests also store a truncated user-agent for operator audit context
- responses are JSON-only
Example request:
GET /webadmin/api/sites
Authorization: Bearer <token>
Accept: application/json
Content-Type: application/json
Capabilities
Super admins choose token capabilities when creating a token from System -> API Tokens, and can later edit a token's name and capabilities without exposing or rotating the token secret. Discovery exposes the saved capabilities without returning the token value, token hash, or token preview. Standard page-building tokens default to these capabilities:
content.readcontent.validatecontent.applynavigation.writeshared-slots.write
Destructive and publish capabilities are separate advanced options and are not selected by default:
content.publishpages.delete
Write endpoints check the relevant capability server-side. Missing capabilities return JSON 403 with api_discovery_url, openapi_url, documentation_url, and example_url guidance. Normal page-building tokens should not include destructive capabilities.
API Model
The API has two complementary modes.
Resource API
Resource endpoints mirror individual admin-equivalent operations:
- list and read pages
- list and read blocks
- list sites, locales, layouts, and block types
- later create or update draft page resources directly
- later list or ensure page slots
- later add, update, move, and delete blocks through resource endpoints
- later add child blocks through resource endpoints
- later manage navigation and shared slots
Phase 1 resource endpoints:
GET /webadmin/api/sites
GET /webadmin/api/locales
GET /webadmin/api/page-layouts
GET /webadmin/api/block-types
GET /webadmin/api/content-contract
GET /webadmin/api/pages
GET /webadmin/api/pages/{page}
POST /webadmin/api/pages/{page}/publish
POST /webadmin/api/pages/{page}/publish-page-owned-blocks
POST /webadmin/api/pages/{page}/slots/{slot}/shared-slot
GET /webadmin/api/blocks
GET /webadmin/api/blocks/{block}
GET /webadmin/api/navigation-menus
GET /webadmin/api/navigation-menus/{navigationMenu}
POST /webadmin/api/navigation-menus
POST /webadmin/api/navigation-menus/{navigationMenu}/items
GET /webadmin/api/shared-slots
GET /webadmin/api/shared-slots/{sharedSlot}
POST /webadmin/api/shared-slots
POST /webadmin/api/shared-slots/{sharedSlot}/blocks
Content Validate / Apply API
Content validate/apply endpoints handle complete multi-step content plans:
POST /webadmin/api/content/validate
POST /webadmin/api/content/apply
validate checks a complete content plan and writes nothing. apply validates the plan again and then creates the requested draft page, navigation items, Shared Slots, Shared Slot block trees, and page slot Shared Slot assignments transactionally. It can also replace named page-owned slots on an existing draft page when the plan uses mode: replace_existing_draft_page and includes an optimistic safety guard. This is useful for AI-generated pages, templates, starter pages, shared headers/footers, and migration helpers where the CMS should avoid half-created content.
The request body may still contain a plan field or another structured content plan payload. The URL should remain /content/validate and /content/apply.
Both modes are needed:
- the Resource API exposes the existing CMS content model and contracts to internal tools
- the Content Validate / Apply API avoids partial writes during larger page builds
Existing Draft Page Slot Replacement
Existing draft page replacement stays inside the validate/apply contract:
POST /webadmin/api/content/validate
POST /webadmin/api/content/apply
Use mode: replace_existing_draft_page to replace one or more page-owned slots on an existing draft page. The operation requires content.validate for validate and content.apply for apply. It does not require pages.delete, because it is not a general page delete operation.
Page Translation path is the canonical public URL. New plans should use paths such as /contact, /features, or /docs/internal-content-api; /p/... is legacy compatibility only. Slash-bearing paths are normalized segment by segment, so /docs/internal-content-api/ becomes /docs/internal-content-api and is not collapsed into docsinternal-content-api. Reserved route areas such as /webadmin, /webadmin/api, /cms, /search, /search.json, /contact-messages, /install, and host auth routes cannot be created as public page paths.
Example:
{
"plan": {
"mode": "replace_existing_draft_page",
"site": "default",
"locale": "en",
"page": {
"id": 9,
"expected_path": "/contact",
"status": "draft"
},
"replace_slots": {
"main": [
{
"type": "plain_text",
"translations": {
"content": "Updated draft contact content."
}
}
]
}
}
}
Rules:
- target page must be in
draftstatus expected_pathorexpected_updated_atis requiredexpected_pathuses the canonical public Page Translation path, not a/p/...legacy alias- the target page must belong to the requested site and locale must be enabled for that site
- each slot must exist on the page and use page-owned blocks
- Shared Slot-backed slots are rejected instead of being cleared
- only blocks in the named
replace_slotsare removed - old blocks are removed and new blocks are written in one transaction
- page revisions are captured before and after apply
- no publish, media fetch/import, broad delete, or Shared Slot assignment clearing happens
Source Sync Metadata
Content plans may persist a limited, secret-safe source_sync object for AI/operator docs sync workflows. Arbitrary page settings are rejected. The accepted shape is:
{
"page": {
"settings": {
"source_sync": {
"type": "markdown_documentation",
"source_id": "webblocks-cms:docs/internal-content-api.md",
"source_path": "docs/internal-content-api.md",
"source_sha256": "64-character-lowercase-sha256",
"managed_slots": ["main"],
"last_synced_at": "2026-06-25T00:00:00Z"
}
}
}
}
Apply persists this metadata to page settings, and page list/detail API responses expose the same allowlisted source_sync fields for future matching. Do not include tokens, environment values, local/server absolute paths, or other secrets.
Explicit Publish Endpoints
Publishing is separate from content apply and requires a token with content.publish.
POST /webadmin/api/pages/{page}/publish
POST /webadmin/api/pages/{page}/publish-page-owned-blocks
POST /webadmin/api/pages/{page}/publish publishes the page record. Its default payload is page-only:
{
"include_page_owned_blocks": false
}
Rules:
- omitted
include_page_owned_blocksbehaves asfalse include_page_owned_blocks: falsepublishes only the page record and leaves draft or in-review blocks unchangedinclude_page_owned_blocks: truepublishes draft and in-review blocks owned by the page's non-shared page slots, including nested child blocks- already published blocks remain unchanged
- Shared Slot-backed slots are excluded and reported in the response
- unsupported Shared Slot cascade fields such as
publish_shared_slots,include_shared_slot_blocks, orshared_slot_cascadereturn JSON422 - the response includes page id/status/path metadata, whether page-owned blocks were included, the count published, excluded Shared Slot summaries, and the page revision id
POST /webadmin/api/pages/{page}/publish-page-owned-blocks publishes only unpublished page-owned blocks and does not change the page workflow status. It uses the same content.publish capability and the same Shared Slot exclusion rule.
AI/operator tools must not assume page publish makes all block content public. Use include_page_owned_blocks: true only when the user explicitly approved publishing all unpublished page-owned blocks for that page. Shared Slot content must be reviewed and published separately.
Content Contract Endpoint
GET /webadmin/api/content-contract is a read-only discovery endpoint for trusted AI/operator tools. It returns the API prefix, validate/apply URLs, admin preview URL template, safety flags, discovery URLs, recommended page-building patterns, and sanitized block contract metadata.
The endpoint is generic CMS product behavior. It must not return install-specific secrets, token values, raw Blade contents, absolute filesystem paths, private server paths, or site-specific instructions. Block contract rows may include handle/slug, label, category, status, container and child support, translatable fields, shared settings fields, and public renderer root behavior.
AI tools should call this endpoint or GET /webadmin/api/block-types before building a plan and must use only handles that are present in the current install.
The contact_form contract includes additional safe form metadata: settings schema, translated fields, the public POST /contact-messages submit endpoint, required CSRF browser behavior, server validation rules, the CMS-owned hidden generated anti-spam check field, generic check-field success behavior, spam classification/quarantine notes, storage-before-notification behavior, recipient fallback order, safe notification failure recording, and /webadmin/contact-messages review behavior. The check field is generated by the renderer, is not part of normal visitor input, and should not be created manually by API or AI/operator tools. Contact-page tools should use that native block rather than Trusted HTML, raw form markup, or mailto: forms. The old website field is no longer the public Contact Form contract.
The human-readable AI Page Building Guide ships in package-native installs at vendor/fklavyenet/webblocks-cms/docs/ai-page-building-guide.md.
Phase 1 Scope
Discovery Endpoints
GET /webadmin/apiGET /webadmin/api/openapi.jsonGET /webadmin/api/ai-guideGET /webadmin/api/examplesGET /webadmin/api/examples/contact-pageGET /webadmin/api/examples/landing-pageGET /webadmin/api/sitesGET /webadmin/api/localesGET /webadmin/api/page-layoutsGET /webadmin/api/block-typesGET /webadmin/api/content-contract
Page Endpoints
GET /webadmin/api/pagesGET /webadmin/api/pages/{page}POST /webadmin/api/pages/{page}/slots/{slot}/shared-slot
Block Endpoints
GET /webadmin/api/blocksGET /webadmin/api/blocks/{block}
Navigation Endpoints
GET /webadmin/api/navigation-menusGET /webadmin/api/navigation-menus/{navigationMenu}POST /webadmin/api/navigation-menusPOST /webadmin/api/navigation-menus/{navigationMenu}/items
Navigation menus use the existing CMS navigation_items.menu_key model. Phase 2A supports the shipped CMS menu handles such as primary, footer, mobile, legal, and docs; it does not add a separate menu table. Creating a navigation menu is treated as creating a safe site-scoped menu group with optional initial items. It refuses to overwrite a site/menu that already has items.
Navigation item URLs may be internal paths such as /, /about, and /contact, or safe http/https URLs. The API rejects javascript:, data:, protocol-relative URLs, traversal, malformed URLs, unsupported targets, and empty labels. Navigation endpoints do not create pages, publish pages, crawl sites, or fetch remote URLs.
Shared Slot Endpoints
GET /webadmin/api/shared-slotsGET /webadmin/api/shared-slots/{sharedSlot}POST /webadmin/api/shared-slotsPOST /webadmin/api/shared-slots/{sharedSlot}/blocks
Shared Slot creation is site-scoped and refuses duplicate handles for the same site. Shared Slot blocks reuse the same block payload writer used by page-owned blocks, so locale-owned copy stays in translation rows and shared settings remain on the block record/settings path. Media import and media assignment remain outside this phase.
Page Slot Assignment
POST /webadmin/api/pages/{page}/slots/{slot}/shared-slot
The endpoint assigns an existing compatible same-site active Shared Slot to an existing page slot. It does not create missing pages or slots. It does not publish the page. It refuses cross-site, inactive, and incompatible Shared Slots. It also refuses to switch a slot that still has page-owned blocks, because Phase 2A does not delete or replace those blocks automatically.
Content Validate / Apply Endpoints
POST /webadmin/api/content/validatePOST /webadmin/api/content/apply
Phase 1 Safety
- draft-only
- no publish through content apply
- no overwrite of existing published content
- no broad overwrite of existing pages or blocks outside
mode: replace_existing_draft_page - no remote fetch
- no media download or import
- no site creation yet
- no destructive page deletion through content apply
- no destructive block deletion outside transaction-scoped draft slot replacement
- no resource update, move, or delete endpoints yet
- no browser session, form, or CSRF requirement for Bearer-token JSON writes
- public unauthenticated access is limited to the minimal
GET /webadmin/apibootstrap response
JSON Error Shape
API errors are JSON-only. They must not redirect to login, render CSRF pages, or expose stack traces. Common fields:
{
"ok": false,
"code": "invalid_internal_api_token",
"message": "Invalid internal API token.",
"api_discovery_url": "/webadmin/api",
"openapi_url": "/webadmin/api/openapi.json",
"documentation_url": "/webadmin/api/ai-guide",
"example_url": "/webadmin/api/examples/contact-page",
"errors": []
}
Expected statuses:
401for missing, invalid, or revoked tokens403for missing capabilities422for validation errors
Resource API Examples
List Pages
GET /webadmin/api/pages
Read Page Details
GET /webadmin/api/pages/{page}
List Blocks
GET /webadmin/api/blocks
Read Block Details
GET /webadmin/api/blocks/{block}
Content Validate / Apply Example
The same payload can be submitted to either endpoint:
POST /webadmin/api/content/validate
POST /webadmin/api/content/apply
Example English marketing homepage draft:
{
"plan": {
"site": "example-site",
"locale": "en",
"layout": "default",
"page": {
"title": "Acme Studio",
"path": "/",
"status": "draft"
},
"slots": {
"main": [
{
"type": "hero",
"translations": {
"title": "Plan, build, and publish with confidence",
"subtitle": "Structured content for modern teams",
"content": "Create a draft homepage from a validated content plan."
},
"children": [
{
"type": "button_link",
"translations": {
"title": "Start planning"
},
"settings": {
"url": "/contact",
"variant": "primary"
}
}
]
},
{
"type": "section",
"children": [
{
"type": "container",
"children": [
{
"type": "grid",
"settings": {
"columns": 3
},
"children": [
{
"type": "card",
"children": [
{
"type": "card_body",
"children": [
{
"type": "plain_text",
"translations": {
"content": "Validate the whole draft before anything is written."
}
}
]
}
]
}
]
}
]
}
]
},
{
"type": "cta",
"translations": {
"title": "Ready to shape the next page?",
"content": "Use structured plans for repeatable content creation."
},
"children": [
{
"type": "button_link",
"translations": {
"title": "Contact us"
},
"settings": {
"url": "/contact",
"variant": "primary"
}
}
]
}
]
}
}
}
Validation Rules
- site handle or ID must resolve
- locale must exist and be enabled for the target site
- layout must exist
- path conflict blocks page creation
- block type must be published and usable
- child support must follow block contracts where available
- user-facing text belongs in translation rows
- shared settings remain shared
- unknown unsafe settings are rejected
- harmless unknown settings may warn or be ignored consistently
- apply validates again before writing
- apply is transactional
- Content apply still rejects publish, site creation, media import, remote fetch, unsupported overwrite, unsupported replace, and delete operations
- navigation and Shared Slot creation are create-only unless a later phase adds explicit draft-safe mutation contracts
Response Shape
Responses should be predictable JSON:
{
"ok": true,
"writes": [],
"data": {
"page": {
"id": 123,
"title": "Product Overview",
"status": "draft",
"edit_url": "/webadmin/pages/123/edit"
}
},
"normalized_plan": {},
"warnings": [],
"errors": []
}
Validation errors should include a path and message:
{
"ok": false,
"writes": [],
"data": null,
"normalized_plan": {},
"warnings": [
{
"path": "plan.slots.main.1.settings.theme",
"message": "Unknown harmless setting ignored."
}
],
"errors": [
{
"path": "plan.page.path",
"message": "A page already exists at this path for the selected site and locale."
}
]
}
Include edit_url where useful for created or updated CMS resources.
Phase 2A Plan Sections
Content plans may include navigation_menus, shared_slots, and page_slot_shared_slots alongside the existing page/slot plan. validate writes nothing. apply writes all valid sections in one transaction and rolls the full plan back when any later section fails.
{
"plan": {
"site": "default",
"locale": "en",
"layout": "default",
"page": {
"title": "Homepage Draft",
"path": "/",
"status": "draft"
},
"slots": {
"main": []
},
"navigation_menus": [
{
"handle": "primary",
"label": "Primary Navigation",
"items": [
{
"label": "Home",
"url": "/",
"target": "_self",
"sort_order": 10
}
]
}
],
"shared_slots": [
{
"handle": "site-header",
"label": "Site Header",
"slot": "header",
"blocks": []
}
],
"page_slot_shared_slots": [
{
"page": "created",
"slot": "header",
"shared_slot": "site-header"
}
]
}
}
page_slot_shared_slots[].page may refer to the page created by the same plan by using created, or to an existing page ID. shared_slot may refer to a Shared Slot created earlier in the same plan or an existing same-site Shared Slot handle.
Future Phases
Phase 2B
- optional draft-safe update/move endpoints for navigation and Shared Slot blocks
- explicit safe clearing/replacement contracts where needed
- deeper header/navbar construction helpers only if they stay generic CMS behavior
Phase 3
- resource endpoints for draft-safe direct page/block edits where needed
- controlled draft updates or draft content replacement
- page assets
- media by existing media ID only
Phase 4
- additional explicit workflow transitions beyond publish when they have separate design and permissions
AI Usage Guidance
- discover sites, locales, layouts, and block types first
- validate before apply
- create draft content
- prefer structured blocks from
docs/public-block-render-markup.md - avoid Safe HTML except as a reviewed fallback
- keep generated public copy in the target language, such as English for an English homepage
Boundaries
- no OpenAI or LLM integration in CMS core
- no crawling or fetching
- no arbitrary import/export replacement
- no automatic publish
- no destructive delete in Phase 1
- no host
/adminroute assumption - no
/cmsroute prefix use - no QuizTem-specific runtime code; QuizTem homepage generation is a later consumer use case for this generic CMS API