Getting started

Scopes

API keys carry a least-privilege set of scopes. Grant only what a key needs.

How scopes work

Every request to /api/v1 authenticates with a bearer key (see Authentication). Beyond authenticating, each key carries a set of scopes — coarse, per-resource grants written as <resource>:<action> (for example ai:invoke or memories:write). A key may only reach an endpoint whose scope it holds. Scopes are stored as a JSON array on the key itself, so two keys under the same app can carry different privileges.

The scope vocabulary is a fixed, compile-time constant — not a table you configure. There is no way to define custom scopes, and no scope confers more than the endpoints listed below. Grants are least-privilege: a freshly minted key holds only the scopes you explicitly requested and nothing is added implicitly.

A scope grants access, not data. Even with guilds:read or personas:read, a key only ever sees resources belonging to its owner (across linked identities). Scopes gate the endpoint; ownership gates the rows.

The wildcard scope is staff-only

A single super-scope, *, satisfies every scope check. It is reserved for internal staff tooling and is never grantable to a developer key. The wildcard exists in the recognized-scope vocabulary but not in the grantable set, so attempting to request it when minting a key is rejected — see Requesting scopes at mint time. You cannot obtain a key that holds *.

Grantable scopes

These are the ten scopes a developer key may hold. Each row lists exactly which endpoints consume the scope. An endpoint that appears under no scope (below) needs none.

ScopeGrantsEndpoints that require it
ai:invoke Run chat completions (token-metered). POST /api/v1/chat/completions
ai:imagine Generate images (metered per image). POST /api/v1/ai/imagine
personas:read Read your AI personas. GET /api/v1/personas
GET /api/v1/personas/{id}
personas:write Create, edit and delete your AI personas. POST /api/v1/personas
PUT PATCH /api/v1/personas/{id}
DELETE /api/v1/personas/{id}
memories:read Read your saved AI memories. GET /api/v1/memories
memories:write Save and delete your AI memories. POST /api/v1/memories
DELETE /api/v1/memories/{id}
guilds:read Read the guilds you belong to. GET /api/v1/guilds
webhooks:read Read webhook deliveries. GET /api/v1/webhooks/deliveries
GET /api/v1/webhooks/deliveries/{id}
webhooks:write Replay webhook deliveries. POST /api/v1/webhooks/deliveries/{id}/replay
usage:read Read API usage and quota. GET /api/v1/usage

For endpoint request and response details, see Chat completions, Images, Personas, Memories, Guilds, Webhooks, and Usage.

Endpoints that need no scope

Three endpoints are reachable by any valid key regardless of its scopes, so quota and identity checks stay available even to a key with an empty scope set:

EndpointNotes
GET /api/v1/ping Liveness probe. Requires no key at all (still subject to the master switch).
GET /api/v1/me Returns the authenticated app, owner, and the key's granted scopes. Requires a valid key but no specific scope.
GET /api/v1/models OpenAI-shaped list of the curated model ids. Requires a valid key but no specific scope.

A key minted with an empty scope array can therefore call /ping, /me and /models only — every other endpoint (including /usage, which requires usage:read) returns 403. Inspect a key's live scopes at any time with GET /api/v1/me:

{
  "data": {
    "app": { "id": "01J8Z...", "name": "My integration", "tier": "free" },
    "owner_user_id": "…",
    "scopes": ["ai:invoke", "usage:read"]
  },
  "meta": { "request_id": "…" }
}

Requesting scopes at mint time

Scopes are assigned when a key is minted and are fixed for the life of that key — there is no “edit scopes” operation. To change a key's privileges, mint a new key with the scopes you want and revoke the old one. Mint from the Developer console at /ops/dev (the Mint key dialog lets you tick the scopes), or call the cookie-authenticated portal endpoint directly:

POST/api/developer/keys
FieldTypeRequiredDescription
appIdstringYesThe app that will own the key.
scopesstring[]NoScopes to grant. Each must be a grantable scope from the table above. Omitted or empty yields a key that can reach only /ping, /me and /models.
labelstringNoHuman-readable name for the key (trimmed to 120 chars).
csrfstringYesCSRF token for this cookie-authenticated portal call.

Every entry in scopes is validated against the grantable set. Blank entries are ignored and duplicates are collapsed. If any entry is the wildcard *, an unknown string, or a reserved scope that no endpoint backs (see below), the whole request is rejected — no key is minted:

HTTP/1.1 422 Unprocessable Entity

{ "error": "invalid_scope" }

On success the key is returned once, with its secret shown a single time:

HTTP
HTTP/1.1 201 Created

{
  "ok": true,
  "key": {
    "id": "9c1e...",              // SHA-256 hash = the opaque key id
    "prefix": "krallo_live_ab12",
    "last4": "9f3d",
    "label": "prod-worker",
    "scopes": ["ai:invoke"],
    "createdAt": 1720900000000,
    "lastUsedAt": null,
    "expiresAt": null,
    "revokedAt": null
  },
  "secret": "krallo_live_ab12…full-secret-shown-once…"
}

The secret is displayed exactly once — only its hash is stored. Copy it immediately. If you lose it, rotate the key. See Authentication for the full key lifecycle.

Reserved scopes

The internal vocabulary declares several additional scopes for forward compatibility — guilds:write, users:read, economy:read, economy:write, orders:read, tickets:read, tickets:write, rmm:read, rmm:write, and notifications:read. No endpoint currently consumes them and they are not grantable. Requesting any of them at mint time returns the same 422 invalid_scope as the wildcard. Treat them as unavailable until a future release adds both the scope to the grantable set and the endpoints behind it.

The insufficient-scope error

When a key is missing the scope an endpoint requires, the call is rejected before any work is done (and before any billing). Native /api/v1 endpoints return 403 with the frozen error envelope, and the error object names the exact scope you needed in required_scope:

HTTP
HTTP/1.1 403 Forbidden

{
  "error": {
    "code": "insufficient_scope",
    "message": "Missing required scope.",
    "required_scope": "ai:invoke"
  },
  "meta": { "request_id": "…" }
}

A 403 insufficient_scope is not retryable as-is — the fix is a key that holds required_scope. Do not confuse it with 401 (missing, invalid, revoked, or expired key) or the 403 app_disabled / 403 owner_suspended account states, which are unrelated to scopes. See Errors for the complete status and code reference.

Request the narrowest scope set that covers your integration. A key that only sends chat needs just ai:invoke; add usage:read if you want to poll quota. Fewer scopes means less exposure if the key leaks.

Next steps