Beta . Ortha CMS is in public beta. Every plan is free until it ends.
Features Ortha CMS on GitHub

API tokens plugin

The lifecycle of the credentials your delivery clients and agents present.

Documents 0.13.1 Updated Edit this page Report a problem

On this page

Bearer tokens for the public API — REST /api/v1/…, GraphQL and MCP. One token works on all three; revoking it closes all three.

The admin page is global, at /api-tokens in the sidebar’s directory group. The routes behind it are served by @orthacms/identity-server.

Install and register

npm install @orthacms/api-tokens-admin
// apps/admin/src/plugins.ts
import { ApiTokensPlugin } from '@orthacms/api-tokens-admin';

ApiTokensPlugin();

No configuration, no environment variables.

Since 0.13.0 the page lists each token with who created it and when, the workspaces it covers, and Last used; a live token within 30 days of its expiry reads Expires soon with a countdown. Active (the default), Expired and Revoked chips filter the list, and Search tokens looks at names and the visible start of the token. New token asks for a name, the workspaces, the access as two cards — Read-only (“Published records only. Right for websites.”) or Full access — and an expiry that starts at 30 days. The secret is shown once with Try it: a ready curl request with both headers, which Copy request copies whole, secret included. Revoke says what stops first — when the token was last used and on which workspaces, and, for full access, that writes stop too.

Scopes

AccessscopePermission keys
Read-onlyreadcontent:read, media:read, segments:read
Full accessfullthe above + content:create, content:update, content:publish, content:delete, media:create, segments:manage
  • The creator’s own permissions are never consulted.
  • Neither scope carries content:approve, webhooks, activity or user keys.
  • Reads serve published entries. Only full may ask for drafts (status=draft|any).
  • A full token behind MCP can publish and delete. Issue the narrowest one.

Using a token

Example language
curl https://cms.example.com/api/v1/content/article \
-H 'Authorization: Bearer orthacms_Xb3k9Q…' \
-H 'X-Workspace-Id: 6b1f0c2e-8a4d-4f7b-9c3e-2d5a7e9f1b40'
const cms = (path, init = {}) =>
  fetch('https://cms.example.com/api/v1' + path, {
      ...init,
      headers: {
          Authorization: 'Bearer ' + process.env.ORTHACMS_TOKEN,
          'X-Workspace-Id': process.env.ORTHACMS_WORKSPACE_ID,
          ...init.headers
      }
  });

const { items } = await (await cms('/content/article')).json();

X-Workspace-Id is required only when the token covers more than one workspace. Copy the id from the workspace’s settings.

What goes wrong:

Statusmessage
401Missing `Authorization: Bearer <token>` header.
401Invalid API token. — unknown, revoked or expired
400This token covers 2 workspaces — name the one you want with the X-Workspace-Id header.
403This token does not cover that workspace.
403This token’s scope does not allow that operation.
{ "statusCode": 401, "message": "Invalid API token.", "error": "Unauthorized" }

Minting from a script

The admin page is the usual way. The same routes work with an administrator’s session (tokens:* keys are admin-only):

curl -c cookies.txt https://cms.example.com/api/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"admin@example.com","password":"…"}'

curl -b cookies.txt https://cms.example.com/api/api-tokens \
  -H 'Content-Type: application/json' \
  -d '{
        "name": "Marketing site build",
        "workspaceIds": ["6b1f0c2e-8a4d-4f7b-9c3e-2d5a7e9f1b40"],
        "scope": "read",
        "expiresAt": "2027-01-01T00:00:00.000Z"
      }'
Body fieldRule
name1–120 characters
workspaceIds1–100 workspace uuids. Unknown id → 400
scoperead or full
expiresAtOptional ISO 8601. Omit = never expires. In the past → 400

Response — the only time secret is returned:

{
    "id": "2e7a4c19-9b3d-4f60-8e21-5c7d0a9b1f38",
    "name": "Marketing site build",
    "workspaceIds": ["6b1f0c2e-8a4d-4f7b-9c3e-2d5a7e9f1b40"],
    "scope": "read",
    "lookupPrefix": "orthacms_Xb3k9Q",
    "expiresAt": "2027-01-01T00:00:00.000Z",
    "lastUsedAt": null,
    "revokedAt": null,
    "createdAt": "2026-09-30T10:12:04.881Z",
    "secret": "orthacms_Xb3k9Q…"
}
curl -b cookies.txt 'https://cms.example.com/api/api-tokens?workspaceId=<uuid>&status=active&q=build&page=1&pageSize=25'
curl -b cookies.txt -X DELETE https://cms.example.com/api/api-tokens/2e7a4c19-9b3d-4f60-8e21-5c7d0a9b1f38   # 204, idempotent
RouteNeeds
GET /api/api-tokenstokens:read
POST /api/api-tokenstokens:create
DELETE /api/api-tokens/:idtokens:delete

The list answers { items, total, page, pageSize, summary }. Since 0.13.0:

  • status is active, expired or revoked — derived from revokedAt and expiresAt, revocation first. q searches the name and the display prefix, case-insensitively.
  • Each item carries createdBy: { id, name, email }.
  • summary is { active, expired, revoked, expiringSoon, nextExpiresAt }, where expiringSoon counts active tokens expiring within 30 days. Only workspaceId narrows it; status and q do not.

A secret is shown once

The secret is orthacms_ + 256 random bits. Only its SHA-256 hash is stored. The list shows lookupPrefix so tokens can be told apart. Lost it? Revoke and mint another.

Audit

Every use is logged as token.used, at most once per token per minute. The list shows lastUsedAt.

See API tokens for presenting a token, and permissions for the keys.