CMS HTTP API reference
The API belongs to one CMS instance/Website. Use the CMS HTTPS origin, not the public site's hostname. The public frontend has files at some API-shaped asset paths but has no editing/publishing API. Routes below are registered in server.go and authentication middleware.
Authentication, requests and errors
Every /api/* route requires a valid encrypted session cookie or a verified
Authorization: Bearer <token>. A Bearer header takes precedence over a cookie;
an invalid Bearer token does not fall back to the cookie. allowedRoles
applies to the whole API. publishRoles, when set, additionally gates publish
and rollback. Roles do not restrict editing to a URI subtree.
For browser mutations, first GET /api/publish/status, retain its CSRF cookie
and csrfToken/X-CSRF-Token, then send the cookie and matching X-CSRF-Token
with POST/PUT/DELETE/PATCH. A supplied Origin (or Referer fallback) must match
request host and scheme. Successfully verified Bearer requests are exempt from
the CSRF cookie/token requirement; same-origin validation still applies to any
supplied Origin/Referer. Logout has independent browser CSRF validation. See
authentication and runnable
CI examples.
JSON requests use Content-Type: application/json. Page writes/rendering and
most JSON bodies are capped at 8 MiB, with specialized limits listed below.
Unknown JSON fields in the normal typed request handlers are rejected. Send
server-generated IDs/timestamps only where explicitly accepted, not by blindly
PUTting an entire GET response. Errors from API handlers normally have
{"error":"message"}; routing/HTTP infrastructure errors may have different
bodies. Always inspect HTTP status before parsing a success payload.
| Status | Typical meaning | Response |
|---|---|---|
| 200 / 201 | Read/update or creation succeeded | Read the route's result |
| 400 | Invalid JSON, path, input or rollback target | Correct the input; do not retry unchanged |
| 401 | Missing, invalid or expired identity | Reauthenticate or obtain a new token |
| 403 | Role denial, CSRF/origin rejection, protected bundled theme | Check the message and permissions |
| 404 | Unknown ID/path | Refresh the current list |
| 405 | Wrong method | Use the documented method |
| 409 | Path/identifier collision or resource in use | Review conflicting resources before replacing/removing |
| 413 | Upload exceeded a size bound | Reduce upload or split work |
| 415 | Unsupported media/content type | Use the supported route/type |
| 500 | Storage, rendering or internal error | Inspect logs/status before retrying mutations |
| 503 | Authentication not ready | Fix configuration/provider availability |
Limits are enforced in route-specific ways: some oversized JSON/import requests return 400 rather than 413. Mutations are not a general multi-request transaction. There is no API version prefix or generic optimistic-lock/ETag write contract; coordinate editors and CI to avoid competing updates.
Identity and build
| Method and path | Result |
|---|---|
GET /api/me |
{email,name,roles,issuer,subject}; roles is an array |
GET /api/version |
{version}; build version, no-store |
issuer plus subject identify an account across session renewal. Never infer
identity equivalence from display name or email alone.
Pages and rendering
| Method and path | Input | Success |
|---|---|---|
GET /api/pages |
none | 200 array of page summaries, empty array when none |
POST /api/pages |
Page input | 201 complete created page |
GET /api/pages/{id} |
URL ID | 200 full page |
PUT /api/pages/{id} |
Page input | 200 full updated page |
DELETE /api/pages/{id} |
URL ID | 200 {ok:true,id} |
POST /api/content/render |
Content plus optional preview context | 200 {html,css} without saving |
POST /api/content/convert |
HTML/Markdown content plus targetFormat |
200 converted content without saving; save with convertFrom |
PUT /api/pages/content?path=...&title=... |
Raw Markdown/HTML; optional menu query | 201 new or 200 replacement page |
DELETE /api/pages/branch?path=... |
Explicit URI root | 200 {count,deleted} |
List summaries contain id, title, normalized path, updatedAt and optional
menu, disabled and optional pageType, sorted by path. Full page responses add content version/format
and the format-specific source/HTML/CSS/object plus optional layout and featured image. GET summaries are
not complete page-update documents.
A normal page input contains title, required path, contentVersion:1 and
format, with format-specific fields. It may include layout, menu and
overwritePageId, plus disabled and featuredImage:{assetId,alt}. Explicit
HTML/Markdown format changes also require convertFrom on updates; see
conversion. It does not take a client-assigned page ID. Example:
{
"title": "About",
"path": "/about",
"contentVersion": 1,
"format": "markdown",
"source": "# About\n\nOur story.\n",
"menu": {"visible": true, "position": 10}
}
For HTML use format:"html", html and optional css, with no nonempty source.
For raw objects, renderer formats and page-type fields (pageType, typeVersion,
fields, pathTemplate) use the extension reference.
layout is an optional layout ID from the selected theme. A supplied menu is a
complete object with visibility and position, plus optional parentId; omitted
menu on updates preserves existing settings. Page updates replace content fields:
send the complete desired format content rather than expecting a partial patch.
A URI collision returns 409 and conflictingPage. To explicitly replace it,
resubmit with its ID as overwritePageId after reviewing data loss. Detailed
format rules and Markdown semantics are in content, and menu
rules in navigation.
Raw URI uploads use text/markdown or text/html and an 8 MiB body cap. Query
parameters are strict; see CI upload semantics.
Branch deletion deletes the matching draft root and all URI descendants,
regardless of menu ancestry. Empty path is refused; explicit / deletes all
saved draft pages. Neither action publishes. Treat branch deletion as a bulk
content change; inspect its scope first.
Settings
GET /api/settings returns the current settings (defaults before first save).
PUT /api/settings is a partial update: omitted fields remain unchanged.
The editable string fields are:
| Field | Default | Meaning |
|---|---|---|
siteTitle |
My Viking Website |
Site-title bindings and default presentation |
description |
empty | Site description binding |
faviconAssetId |
empty | Uploaded raster-image/ICO asset ID for the site favicon; empty removes the favicon link |
seoTitle |
empty | Site-level search metadata title fallback |
seoDescription |
empty | Site-level search description fallback |
themeId |
empty | Selected theme; empty means original page styling |
defaultLayout |
empty (effective default with theme) |
Layout for pages without an override |
The returned object also has server-managed updatedAt. Theme/layout changes
are validated against installed themes and page overrides. A page does not
have its own SEO-description field in the current Page API; do not assume
site SEO fields are per-page controls. The standard Settings modal edits site
title, description and favicon; integrations can use the API for the other fields.
faviconAssetId must identify an existing normal uploaded image: PNG, JPEG,
GIF, WebP, AVIF, BMP or ICO. A URL, missing asset, text file, SVG or raw download
is rejected with 400. The asset cannot be deleted while selected (409); clear
or replace the setting first. Partial updates preserve it when omitted. The
response includes an empty string when cleared. The publisher writes
<link rel="icon" href="/api/assets/<id>/content"> into every page head and
copies the image bytes into the release at that path. Preview responses from
POST /api/content/render and site-page theme previews include faviconUrl
when selected; the editor embeds those image bytes in the preview head. Saving
settings changes drafts; publish separately to update the public site.
Assets
| Method and path | Input | Success |
|---|---|---|
GET /api/assets |
none | 200 asset array, newest first |
POST /api/assets |
Multipart form, file part |
201 asset metadata with id and url |
POST /api/assets?raw=true |
Same multipart form | 201 raw asset, always octet-stream attachment |
GET /api/assets/{id}/content |
URL ID | Bytes with validated type; documents are attachments |
DELETE /api/assets/{id} |
URL ID | 200 {ok:true,id}; 409 for protected in-use references |
Asset metadata includes id, url, name, size, contentType, updatedAt
and optional raw. Limit: 50 MiB of file content, plus up to 1 MiB request
multipart overhead. No base64 JSON upload route exists. Normal upload
extensions/content must pass the allowlist.
Authenticated downloads use Cache-Control: private, max-age=300.
Publication
| Method and path | Input | Success |
|---|---|---|
GET /api/publish/status |
none | Status plus CSRF token/cookie |
POST /api/publish |
No body required | {releaseId,publishedAt,previousRelease} |
POST /api/publish/rollback |
{"releaseId":"000001"} |
Same activation result shape |
Status fields: state (idle, publishing, error), currentRelease,
lastPublishedAt, previousRelease, retainedReleases, csrfToken, and optional
error. Retained-release IDs can expire during confirmation. Publishing is
synchronous at the HTTP API: the request waits for its result. Only the browser
UI saves the open page first; the API publishes already saved state. Rollback
changes public files without restoring drafts. Use publisher
for crash/error/retention semantics.
Themes, packages and WordPress
These larger contracts have their own references:
- Themes API and storage: list/read/save/delete,
samples, unsaved preview, ZIP import/export; authenticated asset delivery is
GET /theme-assets/{hash}/{name}outside/apibut still behind CMS auth. - Extension operations: list/install/export/delete renderer and block packages, and theme ZIP installation.
- WordPress API: WXR inspection, database inspection and review-plan apply; neither inspection nor apply publishes.
OAuth and admin routes
GET /oauth2/start and GET /oauth2/callback are public login routes.
POST /oauth2/logout clears a browser session after CSRF validation and returns
{logoutURL}; GET returns 405. The CMS serves the authenticated editor at /.
The separate admin listener serves /healthz, /readyz and /metrics without
authentication. Port numbers and health bodies are in
observability. Every handler surface carries
the Viking Server header.
