Authentication
One header, two credential types, and how to mint and use a workspace API key.
Every credential travels on one header:
Authorization: Bearer <credential>Two kinds are accepted. They share the header, and that is the only thing they share.
| Realm access token | Workspace API key | |
|---|---|---|
| Looks like | a JWT | sk_zr_... |
| Issued by | the identity provider, when a person signs in | an admin, from Settings or the API |
| Acts as | that person, across every workspace they belong to | one workspace, permanently |
| Authority | the person's live role | borrowed from the person who minted it |
| Limited by scopes | no | yes |
| Expires | with the session | only if you asked it to |
| Use it for | a user-facing app signing people in | a server-to-server integration |
If you are writing an integration, you want an API key. Tokens exist for applications that sign a human in and act on their behalf.
The credential selects the workspace
There is no workspace parameter on any operation. A key is bound to one
workspace; a token resolves to the caller's membership. This is why an id from
another workspace returns 404 rather than 403.
Minting a key
Requires the admin role. Studio has this at Settings → API keys; the
operation is POST /v1/api-keys.
Decide the scopes first
A key with no scopes reaches nothing, and scopes cannot be changed after minting — you mint a new key instead. Read the scope model before you choose. The short version:
- The scope is
<tag>:reador<tag>:write, where the tag is the operation's group —tickets,customers,channels,helpdesk-ops… <tag>:writeimplies<tag>:readon that tag.- The action follows the role floor, not the HTTP method. An admin-floored
GETneeds a write scope.
Create it
curl -sk -X POST https://api.localhost/v1/api-keys \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"name": "billing-sync",
"scopes": ["tickets:read", "customers:read"]
}'{
"id": "01a02869-8310-7487-9f8f-2f9989ea0575",
"name": "billing-sync",
"prefix": "sk_zr_t09Ta",
"scopes": ["tickets:read", "customers:read"],
"expires_at": null,
"last_used_at": null,
"revoked_at": null,
"created_at": "2026-08-22T07:39:58.606643+00:00",
"secret": "sk_zr_t09Tac68i1fdXT5WGKt1WFTm1EaeBRnzEYW8g-0C5xI"
}Add "expires_in_days": 90 to give it an expiry. Without it, the key does not
expire on its own.
Store the secret now
`secret` is returned exactly once
This is the only response that will ever contain it. GET /v1/api-keys returns
the prefix so you can identify the key, and never the secret. If you lose it,
you mint a new key and revoke this one — there is no recovery path.
The prefix (sk_zr_t09Ta) is safe to log. The rest is not.
Using a key
Nothing but the one header. No workspace id, no tenant header:
curl -sk https://api.localhost/v1/tickets?status=open&limit=25 \
-H "Authorization: Bearer $AURALIS_API_KEY"export AURALIS_API_KEY='sk_zr_...'
export AURALIS_HOST='https://your-runtime-host'
curl -s "$AURALIS_HOST/v1/tickets?status=open&limit=25" \
-H "Authorization: Bearer $AURALIS_API_KEY"Note the error handling in the TypeScript tab reads error, not message —
that is the field to branch on. See Errors.
When it does not work
The four you will actually meet, and what each one means.
Never retry a 401 in a loop
Failed credential verifications are counted, 60 per minute per source
address, and the budget is checked before the credential is read. Exhaust it
and correct keys from the same address start getting 429 too. A retry loop on a
bad key takes down your working clients. See
Rate limits.
Rotating a key
There is no in-place rotation. The sequence is mint, migrate, then kill:
last_used_at does not — see below.Revoke (POST /v1/api-keys/{id}/revoke) keeps the row and its history, and
the holder gets api_key_revoked. Delete (DELETE /v1/api-keys/{id})
removes the row entirely, and the holder gets api_key_invalid. Prefer revoke:
it leaves evidence that the key existed and was deliberately retired.
`last_used_at` never gets written
The key record has a last_used_at field and nothing in the runtime ever
populates it. It is null on every key, however heavily used. You cannot use
it to find dormant keys. Check the audit log instead.
Keeping keys safe
- One key per integration, scoped to what that integration does. A shared
*key makes an incident unattributable and un-revocable without breaking everything. - Mint from a service account, not from a founder's login. The key dies when its owner's membership does, so a key owned by someone who might leave is a future outage.
- Never put a key in a browser. It is a full workspace credential with no origin binding. Browser-facing surfaces use the widget and public routes, which need no credential at all.
- The
sk_zr_prefix is deliberately greppable — use it in secret-scanning rules.
What a key cannot do
Two published operations are unreachable by any key at any scope, because they answer about a person rather than about a workspace:
GET /v1/me/workspacesPOST /v1/invitations/{token}/accept
Both return 403 api_key_identity_only. Those need a realm access token.
The full set of limits, including how key authority tracks its owner's role, is in Roles and permissions.