Skip to content

API reference

The routes below are available during authoring and server delivery. They are absent from static production output. These are authenticated editor endpoints; use JavaScript content helpers for public server-rendered pages. The base path defaults to /api/cms and is configurable via apiBasePath.

Status Meaning
200 Request processed; inspect per-entry outcomes for bulk publication
400 Validation error (bad payload, prototype-pollution attempt, malformed field)
401 No valid editor authentication
403 Missing CSRF header or insufficient permissions
404 Entry / collection not found
409 Optimistic-locking conflict (expectedRevision mismatch)
429 Too many failed logins (rate limit) — POST /api/cms/auth/login only, with Retry-After
422 Stored content cannot be read/decoded safely
503 Storage unavailable or required draft support missing
500 Server error
Route Method Purpose
/api/cms/auth/login POST Log in with CARET_EDIT_PASSWORD when password mode is active
/api/cms/auth/session GET Return authentication state and editor identity
/api/cms/auth/logout POST Clear the Caret session and resolve provider logout when configured
POST /api/cms/auth/login
Content-Type: application/json
{ "password": "your-password" }

Sets caret_session cookie on success (HttpOnly; SameSite=Lax; Secure on HTTPS). When an external identity provider is authoritative, password login returns 409 instead of falling back to the shared password.

Route Method Purpose
/api/cms/entries GET List entries by collection
/api/cms/schema GET Collection schema (registered, dynamic, or inferred)
/api/cms/collections-metadata GET List metadata for all dynamic collections
/api/cms/mutate POST Execute a mutation command
/api/cms/publish POST Flush the current editor’s draft overlay into base storage
/api/cms/draft GET / DELETE Read current editor draft status or discard drafts
/api/cms/deployment GET Verified status for the editor’s latest deployment target
/api/cms/history GET / POST Read or restore from revision history
/api/cms/upload POST Upload a file (multipart/form-data)
GET /api/cms/entries?collection=pages
→ {
"collection": "pages",
"entries": [
{ "id": "home", "data": { "headline": "..." }, "revision": 7 },
{ "id": "about", "data": { "title": "..." }, "revision": 2 }
]
}

List responses include pagination with page, pageSize, total, totalPages, hasPrev, and hasNext. Defaults: page 1, 24 entries per page; maximum page size 100. Use id to fetch one entry (still returned in entries) and q to search across IDs and the recognized title fields.

GET /api/cms/entries?collection=pages&page=2&pageSize=24&q=home
GET /api/cms/entries?collection=pages&id=home

Invalid stored values can include validationIssues; an unreadable entry in a list can include readError. Do not interpret either as an empty entry to overwrite.

GET /api/cms/schema?collection=pages
→ {
"collection": "pages",
"source": "explicit", // or "dynamic" or "inferred"
"schema": { /* JSON Schema */ },
"template": { /* default value */ },
"metadata": {
"label": "Pages",
"description": "Site pages",
"icon": "📄",
"creatable": true,
"orderable": false,
"deletable": false,
"singletonId": null,
"order": 10
}
}

See Schemas for the resolution order and supported features.

GET /api/cms/collections-metadata
→ {
"collections": [
{
"id": "products",
"label": "Products",
"icon": "📦",
"creatable": true,
"orderable": true,
"deletable": true,
"order": 20,
"schema": { /* JSON Schema */ },
"created_at": 1714225200000,
"updated_at": 1714225200000
}
]
}

All writes go through here.

POST /api/cms/mutate
Content-Type: application/json
x-caret-request: 1
Cookie: caret_session=...
{ "type": "save_field", "collection": "pages", "id": "home", "field": "headline", "value": "New" }
→ { "ok": true, "revision": 8 }
type Purpose
save_field Update one field on one entry
put_entry Create or replace an entry’s full data
delete_entry Delete an entry
reorder_entries Reorder entries within a collection
update_page_layout Update section composer state
md_block Save one supported rendered Markdown block into the private draft overlay
create_collection Create a dynamic collection (schema + metadata)
delete_collection Delete a dynamic collection (and all its entries + history)
Field Required by Notes
type all One of the values above
collection most Lowercase, alphanumeric, - / _
id most Entry ID
field save_field Dot-path (hero.title); rejects __proto__ etc.
value save_field New value for the field
data put_entry Full entry data object (creates or replaces)
expectedRevision optional on writes Pass for optimistic locking; returns 409 on mismatch

create_collection metadata can include label, description, icon, creatable, orderable, deletable, singletonId, and order. The same capabilities configured for registered collections are enforced server-side.

{
"type": "md_block",
"collection": "blog",
"id": "hello-world",
"blockPath": "3",
"src": "120:168:4f83c21a",
"html": "New <strong>safe</strong> paragraph"
}

The server verifies the source hint, sanitizes the small inline HTML allowlist, derives Markdown itself, and stores the result as a private body draft. Clients never send source Markdown. A stale source range returns 409 with the current revision and requires a fresh render for new stamps.

{ "error": "Revision conflict", "currentRevision": 9 }

with HTTP 409. Re-read the entry, re-apply the user’s intent, and retry with the new currentRevision.

Schema validation failures return HTTP 400 with structured field issues:

{
"error": "Entry does not match collection schema",
"issues": [
{ "path": "gallery.0.width", "code": "too_small", "message": "Must be at least 1" }
]
}
GET /api/cms/history?collection=pages&id=home
→ {
"history": [
{
"ts": 1714225200000,
"data": {...},
"action": "save",
"editor": { "id": "editor_01", "name": "Alex Rivera" }
}
]
}

Capped at the last 50 revisions per entry. action includes save, put, delete, reorder, publish, or restore (the operation that produced the snapshot, not necessarily the mutation type). Markdown publish/restore snapshots can also carry an internal bodySource so the prior prose and frontmatter can be restored together.

Restore an entry to a previous revision:

POST /api/cms/history
x-caret-request: 1
Cookie: caret_session=...
{ "collection": "pages", "id": "home", "ts": 1714225200000 }

Restore writes the stored snapshot and records the state being replaced. Markdown snapshots with source content can restore that source as well. In policy mode it requires edit and publish and writes shared storage. This is not just loading a preview into the form; static delivery still requires rebuild/deploy.

Multipart form with a single file field:

curl
curl -X POST http://localhost:4321/api/cms/upload \
-H 'x-caret-request: 1' \
-H 'Cookie: caret_session=...' \
-F file=@./hero.jpg
→ { "url": "/uploads/abc123.jpg" }

Flush the authenticated editor’s draft overlay into base storage. Optional body scopes the flush:

POST /api/cms/publish
Content-Type: application/json
x-caret-request: 1
Cookie: caret_session=...
{} → publish all draft entries
{ "collection": "pages" } → one collection
{ "collection": "pages", "id": "home" } → one entry
{ "retryRebuild": true } → retry retained deployment request only
→ {
"ok": true,
"published": [{ "collection": "pages", "id": "home", "revision": 8, "deleted": false }],
"conflicts": [],
"failed": [],
"commit": "abc1234", // when CARET_GIT_ON_PUBLISH=true and git repo present
"rebuild": { "triggered": true, "ok": true }
}

retryRebuild sends the stored webhook receipt without republishing content. Responses can include retryAvailable, receiptError, and deploymentTracked; handle these separately from publication outcomes.

Check published, conflicts and failed individually. HTTP 200 / ok: true means the request was processed, not that every entry succeeded. A failure may require finishing a retained recovery plan. Publish recovery explains stale_entry, legacy_draft, recovery_conflict and retry behavior.

When delivery.publish.webhookUrl is configured, publication calls the rebuild hook (POST by default, PUT optional). A deploymentId can correlate it with a deployment status provider.

For Markdown drafts, conflicts can include stale_body when the source range changed or invalid_body when stored draft data is malformed. A conflicted entry is not partially published and its draft remains available.

Returns the authenticated editor’s hasDrafts, count, and, where supported, retryRebuild state. Policy mode can include canPublish. Drafts are scoped to the current identity; this endpoint is not a shared review queue.

Returns normalized provider status for the latest saved deployment target. Without a provider, the response is { configured: false, state: 'unknown' }. Configured responses can include target, state, build ID, provider URL and check time. A provider claim of live needs matching commit or revision evidence. See Deployment status.

Drop draft state without writing to base storage:

DELETE /api/cms/draft
x-caret-request: 1
Cookie: caret_session=...
DELETE /api/cms/draft?collection=pages&id=home → scoped discard
→ { "ok": true, "cleared": ["pages/home"] }
Route Purpose
/admin Login / redirect
/admin/cms Content Studio dashboard
/admin/cms/[collection] Collection list view
/admin/cms/[collection]/[id] Entry editor

These mount at ${mountPath} (default /admin). Disable with enableAdmin: false.

Route Purpose
/__caret/editor.js Inline editor runtime
/__caret/editor.css Editor styles

Loaded automatically when authentication succeeds and the page has a data-caret or data-caret-md binding. Disable with enableInlineEditor: false.

The hosted control plane is not generally available. Its API is intentionally not documented as a production contract yet. Use embedded mode for production and follow release notes for future cloud documentation.