Reference › REST API
Docs/Reference/REST API

REST API

What you'll learn

The HTTP API at /api/cms: endpoints, queries, authentication, CSRF and errors.

Before this page: Local API.

Served at routes.api (default /api/cms). All responses are JSON; access rules always apply.

Collections

MethodPath
GET/:collectionList. Query: where, sort, limit (1–100), page, depth, draft
POST/:collectionCreate (JSON body)
GET/:collection/:idOne document. Query: depth, draft, preview (a preview token: the current draft, no login)
PATCH/:collection/:idUpdate the given fields
DELETE/:collection/:idDelete
GET / POST/globals/:slugRead / update a global
POST/mediaUpload (multipart/form-data, field file), or JSON { url } to download it (from a link)
GET/media/file/:nameA stored file (public)
GET/auth/:provider/loginStarts signing in with a provider (?redirect= an admin page); /auth/:provider/callback finishes. POST /auth/:provider/link (signed in) returns { url } to link an account; GET /auth/identities and DELETE /auth/identities/:id list and unlink them (Single sign-on)
GET/admin/auditFor admins and roles given it: audit log entries, newest first (action, target, doc, actor, from, to, page). /admin/audit.csv exports them; POST /admin/audit/verify checks their signatures (Audit log)
GET/admin/ssoFor admins: the providers, their callback URLs, who may use a password
GET/admin/statusFor admins (and roles given it): the system and what needs attention (Health checks)
GET/admin/rolesFor admins, with auth.rbac: the roles and what can be given. POST /admin/roles with { key, name?, permissions? } adds one; PATCH /admin/roles/:id with { name?, permissions? } changes one; DELETE /admin/roles/:id deletes one; GET /admin/roles/:id/history lists its changes; GET /admin/owned/:userId says what a user owns, by collection (Roles)
GET/admin/backupsFor admins: the backup settings and backups. POST /admin/backups backs up now (202); GET /admin/backups/:id/download downloads one; DELETE /admin/backups/:id deletes one
GET/admin/emailFor admins: the email adapter and its settings (never secrets). POST /admin/email/verify checks the connection; POST /admin/email/test with { to?, locale? } sends a test email now (5 in 10 minutes)
GET/admin/deliveriesFor admins (and roles given it): saved webhook deliveries or emails (kind=webhook|email, state=failed|pending, page). POST /admin/deliveries/:kind/:id/retry and /:kind/retry (all failed) send now; DELETE /admin/deliveries/:kind/:id and /:kind (all failed) delete
GET/:collection/:id/versionsVersions, newest first. Query: limit, page
GET/:collection/:id/versions/:versionOne version with its data
POST/:collection/:id/versions/:version/restoreRestore a version
POST/:collection/:id/unpublishUnpublish (collections with drafts)
POST/:collection/:id/discard-draftDiscard the pending draft
GET / POST/:collection/:id/schedulePending scheduled jobs / schedule { action, at }
DELETE/:collection/:id/schedule/:jobCancel a scheduled job
GET / POST/jobs/runRun due scheduled jobs and webhook retries (cron secret or admin)
POST/:collection/:id/previewLive preview: { doc, url } for unsaved changes (/:collection/preview for a new document)

Globals have the same version routes under /globals/:slug/… (versions, versions/:version, versions/:version/restore, unpublish, discard-draft, preview, schedule). Version and preview routes need update access.

where uses brackets or JSON:

GET /api/cms/posts?where[status][equals]=published&where[views][gte]=10&sort=-createdAt&limit=20
GET /api/cms/posts?where={"or":[{"featured":{"equals":true}},{"views":{"gt":100}}]}

in / not_in accept comma-separated values, exists takes true/false, and equals=null matches empty values. draft=true only works for logged-in users. With localization, locale (th, en… or all) and fallback-locale=false work on every read and write.

Authentication

MethodPath
POST/users/login{ email, password } → session cookie, { user, exp, csrfToken }
POST/users/logoutEnds the session
GET/users/me{ user, csrfToken } for the current session
GET/users/init{ hasUsers }
POST/users/first-registerCreates the first admin while there are no users
GET/users/signupWith auth.members.signup: { token, turnstile, verifyEmail } for a sign-up form (Site members)
POST/users/signup{ email, password, name?, token } → 202 { verify: true }, or a session without email confirmation
POST/users/verify-email{ token } from the confirmation email → confirms it and signs in

Browsers send the session cookie. Every POST, PATCH, PUT and DELETE made with the cookie must include the CSRF token as x-csrf-token (from the login response, GET /users/me or the ecms-csrf cookie), and come from the API's own origin or one in auth.trustedOrigins.

Servers and apps send Authorization: Bearer <token> with the session token; no CSRF token is needed.

CORS

Browser code on another origin can call the API when that origin is listed in cors (or in auth.trustedOrigins, which also allows cookies). Preflight OPTIONS requests are answered for those origins; other origins get no CORS headers, so the browser blocks the response.

Errors

{ "errors": [{ "message": "is required", "field": "title" }] }

Statuses: 400 (validation, bad query), 401, 403, 404, 405, 413, 415, 429 and 500. In production, 500 responses don't include error details.

Next steps