Domma CMS User Manual

External API & Tokens

Updated by Darryl Waterhouse on 29 September 2026 4 min read

The external API is the stable surface for other systems: /api/v1/:slug reads and writes a collection's entries under that collection's own access rules. It shares its handlers with /api/collections/:slug/public, so both behave identically. For fixed, named queries at clean URLs see API Builder.

Endpoints

MethodPathVerb checkedSuccess
GET/api/v1/:slugread{ entries, total, page, limit }
GET/api/v1/:slug/:idreadThe entry
POST/api/v1/:slugcreate201 + the entry
PUT/api/v1/:slug/:idupdateThe entry
DELETE/api/v1/:slug/:iddelete{ "success": true }

Write bodies are { "data": { ... } }. PUT replaces the entry's data wholesale - send every field, not just the changed ones. Entries are validated against the collection's fields (400 with the reason); number fields sent as decimal text are stored as numbers.

List query parameters

ParameterDefaultDescription
page1Page number
limit50Page size. 0 is treated as the default, not as "everything"; ask for a larger number instead.
sort / ordercreatedAt / descSort field and direction (asc or desc)
search-Substring match across all field values
filter[<field>]-Equality filter; add an operator suffix: _ne, _gt, _gte, _lt, _lte, _in, _nin, _contains, _starts, _ends, _exists. Filters AND together; dot paths reach nested data.
resolveRefs-true or 1 adds referenced entries under _refs (list only)
scope=mine-Only the caller's own entries. Needs a JWT and skips the read access rule.
curl 'https://example.com/api/v1/jobs?filter[status]=open&filter[salary_gte]=40000&sort=postedAt&order=desc&limit=20'

Access rules

Each verb is set on the collection under API & Export in the collection editor, stored as schema.api.<verb>:

"api": {
  "read":   { "enabled": true,  "access": "public", "fields": ["title", "location", "salary"] },
  "create": { "enabled": true,  "access": "token" },
  "update": { "enabled": true,  "access": "admin" },
  "delete": { "enabled": false }
}
accessWho may call
publicAnyone, no credentials.
tokenOnly a valid project API token (below). A JWT is refused.
a role nameA signed-in user (JWT) holding that role or a more senior one, across all their roles. A name that is not a role on the site admits only the level-0 role. An API token is refused.
  • A verb that is not enabled answers 403.
  • read.fields is an allowlist of data fields returned by external reads; empty means every field. Admin endpoints are unaffected. _refs is not filtered.
  • If the collection's project is switched off, every /api/v1 call on it answers 404.
  • Role and token modes grant the verb on every entry - there is no per-row ownership check on update or delete.
  • Entries created here get meta.source: "api" and meta.createdBy set to the user's id, token:<token id> for a token, or null.

API tokens

Tokens are machine credentials, created under System > API Tokens (permission family api-tokens.*) or by a scaffolder recipe. They are stored as entries in the file-based api-tokens preset collection; only a SHA-256 hash and the last four characters are kept.

  1. Open System > API Tokens and choose New token.
  2. Give it a name and pick its project. The project cannot be changed later.
  3. Optionally add scopes, one line per collection: jobs: read, enquiries: create, read. No lines means every collection in the project; a collection with no verbs means all four.
  4. Optionally set expires. From then on every call with it is refused; the sidebar badge warns 14 days ahead and turns red once one has expired.
  5. Save and copy the token now - dcms_ followed by 64 hex characters. It is never shown again; if it is lost, revoke it and make another.

From the list you can switch a token off (refused until switched back on), edit its name, scopes and expiry, or revoke it (deleted, refused at once). lastUsedAt is updated at most once a minute.

A call with a token is accepted only when all of these hold, otherwise it is refused:

CheckRefusal
The verb's access is tokenrole mode: 401 (a token is not a JWT); public mode ignores it
Header is exactly Authorization: Bearer dcms_<64 lower-case hex>401 API token required
Token exists, is switched on and has not expired401 Invalid, disabled or expired API token
Token's project is the collection's project (untagged collections are core)403 Token is not valid for this collection's project
Scopes list this collection and verb (or the token has no scopes)403 Token scope does not permit this operation

Sending a token

# Read (collection "jobs" has read.access = "token")
curl -H 'Authorization: Bearer dcms_0123...cdef' \
  'https://example.com/api/v1/jobs?filter[status]=open'
# Create
curl -X POST https://example.com/api/v1/enquiries \
  -H 'Authorization: Bearer dcms_0123...cdef' \
  -H 'Content-Type: application/json' \
  -d '{"data":{"name":"Ada","email":"ada@example.com","message":"Hello"}}'
# Replace an entry (send every field)
curl -X PUT https://example.com/api/v1/jobs/3f2c... \
  -H 'Authorization: Bearer dcms_0123...cdef' \
  -H 'Content-Type: application/json' \
  -d '{"data":{"title":"Engineer","status":"closed"}}'
# Delete
curl -X DELETE https://example.com/api/v1/jobs/3f2c... \
  -H 'Authorization: Bearer dcms_0123...cdef'

A role-mode verb takes a user's access token instead (from POST /api/auth/login). Keep tokens on the server: anything shipped to a browser can be read by its user.

Managing tokens over HTTP

All need a JWT; project scope applies (a user confined to projects sees and manages only their tokens, and gets 403 for others).

MethodPathPermissionDescription
GET/api/api-tokensapi-tokens.readList tokens (never the hash)
POST/api/api-tokensapi-tokens.createCreate; 201 with { token, plaintext }
PUT/api/api-tokens/:idapi-tokens.updateChange name, enabled, scopes, expiresAt
DELETE/api/api-tokens/:idapi-tokens.deleteRevoke
// POST /api/api-tokens
{ "name": "mobile-app", "project": "core",
  "scopes": [{ "collection": "jobs", "verbs": ["read", "create"] }],
  "expiresAt": "2027-01-01T00:00:00.000Z" }
// Response 201 - plaintext appears here and nowhere else
{ "token": { "id": "uuid", "name": "mobile-app", "project": "core", "tokenHint": "cdef",
             "scopes": [...], "enabled": true, "expiresAt": "2027-01-01T00:00:00.000Z",
             "lastUsedAt": null, "createdBy": "Alice", "meta": {...} },
  "plaintext": "dcms_..." }

The base super-admin and admin roles hold api-tokens.*. Custom roles are not given it automatically - grant it in the role editor.

Every route shares the site-wide rate limit of 500 requests a minute per IP. Browser calls from another origin also need that origin allowed in cors in config/server.json.