Contents
- Domma CMS User Manual
- 1. Using the CMS
- 2. Tutorials
- 3. Components
- 4. API Reference
- 5. Tools
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
| Method | Path | Verb checked | Success |
|---|---|---|---|
GET | /api/v1/:slug | read | { entries, total, page, limit } |
GET | /api/v1/:slug/:id | read | The entry |
POST | /api/v1/:slug | create | 201 + the entry |
PUT | /api/v1/:slug/:id | update | The entry |
DELETE | /api/v1/:slug/:id | delete | { "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
| Parameter | Default | Description |
|---|---|---|
page | 1 | Page number |
limit | 50 | Page size. 0 is treated as the default, not as "everything"; ask for a larger number instead. |
sort / order | createdAt / desc | Sort 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 }
}
access | Who may call |
|---|---|
public | Anyone, no credentials. |
token | Only a valid project API token (below). A JWT is refused. |
| a role name | A 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
enabledanswers403. read.fieldsis an allowlist ofdatafields returned by external reads; empty means every field. Admin endpoints are unaffected._refsis not filtered.- If the collection's project is switched off, every
/api/v1call on it answers404. - 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"andmeta.createdByset to the user's id,token:<token id>for a token, ornull.
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.
- Open System > API Tokens and choose New token.
- Give it a name and pick its project. The project cannot be changed later.
- 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. - 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.
- 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:
| Check | Refusal |
|---|---|
The verb's access is token | role 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 expired | 401 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).
| Method | Path | Permission | Description |
|---|---|---|---|
GET | /api/api-tokens | api-tokens.read | List tokens (never the hash) |
POST | /api/api-tokens | api-tokens.create | Create; 201 with { token, plaintext } |
PUT | /api/api-tokens/:id | api-tokens.update | Change name, enabled, scopes, expiresAt |
DELETE | /api/api-tokens/:id | api-tokens.delete | Revoke |
// 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.