Examples on this page use $API_URL. Set it once, with the /v1 prefix
included — every route lives under it:
export API_URL=https://<your-api-host>/v1A workspace session has two credential tiers — tenant credentials and the workspace token. This guide covers moving between them and keeping a session healthy. (A tenant can also mint a durable scoped API key for an end user.)
Tenant credentials
Two headers, on six routes — creating, listing, and deleting workspaces, reading quota, minting a token, and minting a scoped API key:
-H "X-Api-Key: $API_KEY" -H "X-Api-Secret: $API_SECRET"Missing or wrong credentials return 401 with code bad_api_key.
Mint a token
curl -X POST "$API_URL/auth/token" \
-H "X-Api-Key: $API_KEY" \
-H "X-Api-Secret: $API_SECRET" \
-H "content-type: application/json" \
-d '{"workspace":"demo","password":"…","scopes":["read","sign"]}'scopes is optional; omitting it grants ["read","write","sign"]. Request the
narrowest set you need — see Scopes and permissions. Only
read, write, sign, and export are grantable, and sending [] is
rejected rather than read as “no scopes”.
A 201 returns the token and its expiry:
{
"token": "eyJhbGciOi...",
"expiresAt": "2026-08-18T12:34:56.000Z",
"workspace": "demo",
"scopes": ["read", "sign"]
}Use it as a bearer token on every other route:
-H "Authorization: Bearer $TOKEN"Refresh before the idle clock runs out
curl -X POST "$API_URL/auth/token/refresh" \
-H "Authorization: Bearer $TOKEN"The body must be empty. No body at all and a literal {} are both accepted;
anything with a field in it returns 400 with invalid_body. The response adds
sessionExpiresAt, the absolute ceiling that refresh cannot move:
{
"token": "eyJhbGciOi...",
"expiresAt": "2026-08-18T12:49:56.000Z",
"sessionExpiresAt": "2026-08-18T20:15:00.000Z",
"workspace": "demo",
"scopes": ["read", "sign"]
}Each refresh returns a new token for the same lease. Replace the one you
hold — but note the previous JWT is not cancelled by the exchange. It keeps
working until its own expiresAt, because refresh extends the lease rather
than rotating it. Treat refresh as buying time, never as containing a leak.
Handle expiry
An expired session returns 401 with session_expired. The recovery is always
the same: mint a new token with the workspace password and retry. A robust
client treats this as routine rather than exceptional.
Note that refresh cannot rescue a session past its absolute ceiling — once
sessionExpiresAt is in the past, only a fresh mint works.
Revoke when finished
curl -X DELETE "$API_URL/auth/token" \
-H "Authorization: Bearer $TOKEN"Returns 204. This revokes the lease of the token you sent, and only that
one. Other tokens minted for the same workspace keep working — the workspace
locks when its last lease goes.
That makes revoke the tool for containing a specific token: send the compromised one with itself. It is not a way to end everything at once.
Failures worth handling
| Code | Status | What happened |
|---|---|---|
invalid_body |
400 | Malformed mint body, an unrecognised scope name, an empty scopes array, or a refresh body that was not empty. A body rejected on its shape names the offending fields; an unrecognised scope names the grantable set instead; a non-empty refresh body just says so |
invalid_slug |
400 | workspace is not a lowercase slug, or is one of the reserved names (con, prn, aux, nul) |
bad_api_key |
401 | Tenant credentials missing or wrong |
bad_password |
401 | Workspace password wrong |
workspace_not_found |
404 | No such workspace for this tenant |
session_expired |
401 | Idle or absolute clock elapsed, the lease was revoked, or the token did not verify |
scope_denied |
403 | Valid token, but the scope was not granted |
mint_rate_limited |
429 | The tenant’s key-derivation budget is spent. Account unlocks spend it too |
session_capacity_exceeded |
429 | A capacity ceiling was reached. details.scope says which — tenant or process for open workspaces, workspace for live tokens on one workspace — and details.limit gives the number |
mint_rate_limited carries details.retryAfterSec and a matching retry-after
header, so a client can back off without reading the body.
session_capacity_exceeded carries neither — it clears when capacity is
released, not after a fixed wait, so retry on your own schedule.
Related
- Workspaces and tokens — the model behind this
- Auth endpoints — exact shapes