AuralisDocs

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 tokenWorkspace API key
Looks likea JWTsk_zr_...
Issued bythe identity provider, when a person signs inan admin, from Settings or the API
Acts asthat person, across every workspace they belong toone workspace, permanently
Authoritythe person's live roleborrowed from the person who minted it
Limited by scopesnoyes
Expireswith the sessiononly if you asked it to
Use it fora user-facing app signing people ina 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>:read or <tag>:write, where the tag is the operation's group — tickets, customers, channels, helpdesk-ops…
  • <tag>:write implies <tag>:read on that tag.
  • The action follows the role floor, not the HTTP method. An admin-floored GET needs 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:

Mint the replacement with the same scopes.
Deploy it. Both keys work at once — there is no limit on concurrent keys beyond 64 scopes each.
Confirm traffic has moved. The audit log records key use; last_used_at does not — see below.
Revoke the old key.

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/workspaces
  • POST /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.

On this page