Skip to content

How to authenticate and keep a session alive

Mint, refresh, and revoke workspace tokens, and handle expiry without surprises.

Updated View as Markdown

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>/v1

A 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.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close