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.
Conventions
Section titled “Conventions”HTTP status codes
Section titled “HTTP status codes”| 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/loginContent-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.
GET /api/cms/auth/session
→ { "authenticated": true, "identity": { "id": "editor_01", "name": "Alex Rivera", "roles": ["editor"] }}Used by the inline editor bootstrap to decide whether to load editor JS.
POST /api/cms/auth/logoutClears the cookie. A request with Content-Type: application/json returns
{ ok: true, redirect: "..." }; form/default requests redirect with HTTP 302.
The redirect can point to the identity provider logout URL. No CSRF header is required.
Content
Section titled “Content”| 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
Section titled “GET /api/cms/entries”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=homeGET /api/cms/entries?collection=pages&id=homeInvalid 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
Section titled “GET /api/cms/schema”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
Section titled “GET /api/cms/collections-metadata”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 } ]}POST /api/cms/mutate
Section titled “POST /api/cms/mutate”All writes go through here.
POST /api/cms/mutateContent-Type: application/jsonx-caret-request: 1Cookie: caret_session=...
{ "type": "save_field", "collection": "pages", "id": "home", "field": "headline", "value": "New" }
→ { "ok": true, "revision": 8 }Mutation command types
Section titled “Mutation command types”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) |
Common fields
Section titled “Common fields”| 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.
Markdown block mutation
Section titled “Markdown block mutation”{ "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.
Conflict response
Section titled “Conflict response”{ "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
Section titled “GET /api/cms/history”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.
POST /api/cms/history
Section titled “POST /api/cms/history”Restore an entry to a previous revision:
POST /api/cms/historyx-caret-request: 1Cookie: 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.
POST /api/cms/upload
Section titled “POST /api/cms/upload”Multipart form with a single file field:
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" }POST /api/cms/publish
Section titled “POST /api/cms/publish”Flush the authenticated editor’s draft overlay into base storage. Optional body scopes the flush:
POST /api/cms/publishContent-Type: application/jsonx-caret-request: 1Cookie: 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.
GET /api/cms/draft
Section titled “GET /api/cms/draft”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.
GET /api/cms/deployment
Section titled “GET /api/cms/deployment”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.
DELETE /api/cms/draft
Section titled “DELETE /api/cms/draft”Drop draft state without writing to base storage:
DELETE /api/cms/draftx-caret-request: 1Cookie: caret_session=...
DELETE /api/cms/draft?collection=pages&id=home → scoped discard→ { "ok": true, "cleared": ["pages/home"] }Admin pages
Section titled “Admin pages”| 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.
Editor assets
Section titled “Editor assets”| 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.
Cloud mode Alpha
Section titled “Cloud mode ”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.