Skip to content
Knowledge sections

API tokens

Workspace-scoped credentials with expiry, guided replacement, and revocation.

tokensauthsecurity
On this page

API Tokens authenticate REST and MCP requests. Each token belongs to one Workspace.

Creation

A workspace owner creates a token on the API tokens page at /workspaces/:slug/api-tokens. Creation requires apiToken:create; REST callers use POST /workspaces/:slug/api-tokens with an admin-scoped token. Both paths call ApiTokenRegistry.create.

Choose scopes and optionally a future expiry. The web form labels the expiry as UTC. REST accepts a canonical ISO UTC timestamp, such as 2027-12-01T00:00:00.000Z; omit expiresAt for no expiry. Copy the token when it appears. Its plaintext is returned once; storage retains only its SHA-256 hash and a display prefix. Creation records an api_token.created Audit Event.

Scopes

The scopes are read, write, and admin. They map to permissions through the same authorization rules as workspace roles. Every route and MCP tool checks its own permission after authenticating the token. A write token cannot create or replace credentials because those actions can grant more authority.

A missing, unknown, revoked, or expired token gets HTTP 401. A valid token without the requested permission gets HTTP 403. Tokens cannot access another Workspace, even if they carry the admin scope.

Expiry and replacement

Every bearer verification checks expiry. A token stops authenticating at the exact expiresAt instant. Expired tokens remain listed with an expired state.

To rotate a usable token, select Replace, keep or remove scopes, and choose how long the old credential can overlap with its replacement. The maximum is 24 hours; choose 0 seconds to retire it immediately. Copy the replacement, update your clients, and revoke the old credential early when all clients have switched. The form shows the exact retirement time. An earlier existing expiry always wins.

The replacement keeps the original workspace, name, and expiry. REST and SDK callers can request an earlier future expiry, but cannot extend it. You cannot replace a revoked, expired, or already replaced token. Replacement works at the plan's token ceiling because it transfers the original token's slot.

POST /workspaces/:slug/api-tokens/:tokenId/replace accepts scopes, overlapSeconds, and optional expiresAt. It requires apiToken:create. The response includes the new plaintext token, previousTokenId, and previousTokenExpiresAt. D1 commits the old credential's retirement, the new hash, and an api_token.replaced Audit Event together. That event links both IDs and their expiry details without storing plaintext. Replacement and creation are excluded from MCP tools to keep credentials out of tool histories.

Revocation

Revocation requires apiToken:revoke and takes effect immediately. Subsequent requests get HTTP 401, and the token disappears from the list. It also records an api_token.revoked Audit Event. Revoking an old token does not revoke the replacement; revoke each credential that needs to be retired.

Using the SDK

The Typed SDK derives its request and response types from the REST contract. Store the replacement in your secret manager when you receive it.

import { createStarterClient } from '@b2b-saas-starter/sdk'
 
const client = createStarterClient({
  baseUrl: 'https://api.example.com',
  apiToken: adminToken
})
 
const replacement = await client.apiTokens.replace('acme', tokenId, {
  scopes: ['read'],
  overlapSeconds: 3600
})
 
// Save replacement.token now. It cannot be retrieved later.

See Typed SDK for the Effect-native client and error model.