API keys and access
The three key kinds, scopes, custom roles with per-content-type and per-field rules, personal access tokens, and how a key is checked on every request.
Every request to the API or the MCP server carries a bearer token:
curl -s "$API/auth/me" -H "Authorization: Bearer $TOKEN"GET /auth/me returns who the token is and what it may do, which makes it the quickest check that
a key works. A missing or unknown token is answered with 401; a valid token without the scope a
route needs, or used against another space, with 403.
Key kinds
Section titled “Key kinds”| Kind | Name | Use it for |
|---|---|---|
cma |
Management | Integrations that write: imports, sync jobs, CI, admin tooling |
cpa |
Preview | Staging sites and previews that show drafts |
cda |
Delivery | Production sites and apps: published content only |
Ship only Delivery keys in anything a visitor can inspect (a browser bundle, a mobile app); they can’t read drafts or change anything. Each kind gets default scopes; a key can be minted with a narrower list.
Create keys in Settings → API keys, or with a key that holds space:admin:
curl -s "$API/spaces/$SPACE/api-keys" \ -H "Authorization: Bearer $ADMIN_KEY" -H 'content-type: application/json' \ -d '{ "kind": "cda", "name": "Website (production)" }'The raw token is in the response once. contentworker stores only a hash of it, so a lost token
can’t be recovered: revoke it and mint another. Tokens start with cwk_ and the kind, so a leaked
one is easy to recognize and scan for.
Scopes
Section titled “Scopes”Scopes are what a key may do: content:write to edit drafts, content:publish to publish,
content:manage to change the content model, preview:read and delivery:read to read, and so
on. Every route checks one, against the space in its path. The full list, with what each allows,
is in Permission scopes.
A role narrows a key further, per content type and per field. It has a set of scopes and a list of grants:
{ "name": "Blog writer", "scopes": ["content:write", "preview:read"], "contentGrants": [ { "contentTypeApiId": "article", "actions": ["read", "write"], "readOnlyFields": ["slug"] }, { "contentTypeApiId": "author", "actions": ["read"], "deniedFields": ["email"] } ]}- A key bound to a role can only touch the content types its grants name (
*matches any type; an exact grant wins over*). Types without a grant are invisible to it, in lists too. - Denied fields are removed from every response and refused on write. Read-only fields can be read but not written.
- Roles are live: editing a role changes what every key bound to it can do on the next request.
Create roles in Settings → Roles or at POST /spaces/{space}/roles, then bind a key with
{ "kind": "cma", "roleId": "…" }.
Every space also has five built-in roles, Viewer, Approver, Editor, Manager and Administrator, which you can assign, edit and clone like your own; see Team and roles.
Personal access tokens
Section titled “Personal access tokens”A personal access token acts for a person rather than an integration: it carries their
identity in the audit log and can never do more than they can. Create one for a script or CI job
at POST /spaces/{space}/personal-access-tokens with a name, scopes, and optionally an expiry of
up to a year (expiresInDays). If the person’s role is later narrowed, the token narrows with it.
AI clients connecting over MCP get a token the same way through an approval screen; see MCP server.
Errors
Section titled “Errors”Every error body has the same shape:
{ "error": { "code": "validation_failed", "message": "…", "issues": [{ "field": "title", "message": "Required" }] } }| Status | When |
|---|---|
| 401 | No token, or one that isn’t valid |
| 403 | The token lacks the scope, the content-type grant, or the space |
| 404 | No such entry, type, release… |
| 409 | A conflict: a unique value already taken, a delete blocked by links |
| 422 | Validation failed; issues lists each field and why |