Account
Keys and scopes
A key is created with the read scope unless you say otherwise, and a read key authenticates fine and then fails every completion.
Live Built and working today on the surface this card describes.
What it does
Keys are created, listed and revoked from the developer portal, or from the same endpoints the portal calls, under /api/developer/keys. Those endpoints take a Labs session, Authorization: Bearer <access token> or the session cookie, with X-Workspace-ID naming the workspace the key belongs to. A gateway key cannot mint or revoke keys: an ai-scoped key is confined to /api/prima/v1/ and answers 403 anywhere else.
The trap is the default. Creating a key from a Labs session without naming a scope gives you read. A read key is a real key, so it is found, and then every gateway call, GET /models included, answers 403 with code: insufficient_scope. That reads as a broken gateway to anyone who has not read the code, which is exactly what makes it expensive.
The scope a gateway key needs is ai. Ask for it explicitly, every time. From a Prima surface, a session whose token names prima_web, prima_chat or prima_dev, which is what the developer portal and the chat mint, the endpoint refuses with 403 anything but exactly ["ai"]. Leaving scopes out is not the same as asking for ai: the omitted field becomes ["read"] first, and that is then refused too. Nothing is forced or rewritten; the request either names ["ai"] or fails. The read default only produces a key from a Labs session.
The key is shown once, when it is created. What is listed afterwards is a prefix, not the key.
A gateway key spends money and is deliberately not a general credential. It cannot read your stone repository: that endpoint refuses any key, of any scope, and takes a signed-in session instead. It cannot change the billing instruction either: PUT and DELETE /credits/auto-topup, and a top-up with save_card: true, answer 403 session_required to a key and take the session. The key may read the instruction, the tiers, the history and /usage, and may open a plain top-up when its user is the workspace owner or an admin; a member's key is 403 owner_required there.
Inputs and outputs
| Create | POST /api/developer/keys with a name, a description and scopes: ["ai"], sent with the session and X-Workspace-ID. The key comes back once. Without a workspace context, which on api.prima.li means without the header, it is a 400; from a Prima surface with any scope list but ["ai"] it is a 403; with a scope name outside read, write, delete, ai it is a 400 before the surface rule runs. |
|---|---|
| List | Prefixes, names, scopes and usage. Never the key. |
| Revoke | DELETE /api/developer/keys/{id}. Answers 404 for a key that is already gone, which is the same outcome. |
| Prefix | prima_pk_. Older deap_pk_ rows exist in the account service's key table; whether one still authenticates on the gateway has not been checked. |
| Scope required by the gateway | ai, and only ai. A key carrying admin, full or * is refused by the gateway with 403 scope_not_recognised: it defines no wildcard. The account service's billing paths accept those three as satisfying ai, so a wildcard key would work for billing and fail for inference. Do not create one. |
Configuration
| Scopes | Named at creation. read, write, delete and ai are the ones a self-service key may carry; ["read"] is the default with a Labs session, and is not what a gateway key wants. Anything else is refused with a 400 naming the allowed set. |
|---|---|
| One key per thing | The chat mints one key per session, scopes: ["ai"], and revokes it at sign-out. The developer portal does the same on the w6-konto branch, not yet deployed: one key per sign-in, named after the session, with a 24 hour expiry, for /usage and the billing reads a key may make, revoked at sign-out; the session-only writes keep the session. The desktop tool is designed to mint one per device. A key you can revoke on its own is worth more than a key you share. |
| Where to keep it | An environment variable, or a file only your user can read. Not a command-line flag, which lands in shell history and in the process table. |
Limits
- There is no token revocation on the estate today, and an access token lasts 60 minutes. Signing out does not end a gateway session. Revoking the key is what ends it.
- An
aikey could change and delete the auto top-up instruction until 2026-09-11. Sincealchemy_labse428dab on the auto top-up branch it cannot: those writes answer 403session_requiredto a key,type: permission_error,code: AUTHZ_001. See the credits card for which session may make them. - Which session: the workspace owner's, or that of a member whose role is
admin. Any other member is refused with 403owner_required, same envelope, and the same answer meets amember's key on a plain top-up, the one write a key may make. Decided 2026-09-11 (A-1) and built atalchemy_labs3d21356 onw1-auto-topup:_require_billing_roleruns after the session guard on the three/creditswrites, owner byworkspaces.owner_idor an activeowner/adminmembership row;member,viewer,desk_requesterand unknown roles are refused; reads are untouched. Not deployed.owner_requiredis final for that person: show "ask the workspace owner or an admin" and disable the write, do not retry. - Keys created by self-serve signup did not reach the gateway for a period and answered 401 forever. That is fixed. If a key from that period still fails, create a new one.
- A key's IP allowlist is enforced by the gateway and fails closed: a key with
allowed_ipsset is refused with 401 whenever the peer address is missing or not in the list. Behind the proxy the gateway sees only the proxy's address, so setting the column locks the key out on every request. Leave it unset until a trusted-proxy policy exists.
Example
create a key that will actually work
POST /api/developer/keys
Content-Type: application/json
{"name": "my laptop",
"description": "…",
"scopes": ["ai"]} Source of truth
philosophers_stone/platform/primaapi.md § Open items (the `scopes: ["read"]` default and why a read key reads as a broken gateway)philosophers_stone/platform/primaapi.md § Self-serve signup mints a key Prima cannot see (fixed, and what it looked like)philosophers_stone/platform/primaapi.md § The auth seam, as built (no working revocation, 60-minute access token)prima/prima/auth/keys.py (`AI_SCOPE`; `WILDCARD_SCOPES` denied with 403 `scope_not_recognised`; a key without `ai` is 403 `insufficient_scope`; the key shape check; `check_key` enforces `allowed_ips` and fails closed on an unknown peer)prima/prima/api/auth.py (`_client_ip`: the peer address only, no `X-Forwarded-For`, so behind the proxy a set allowlist denies every request and the column must stay NULL)alchemy_labs/backend/api/routers/developer_router.py (`create_api_key`: `get_current_user` plus `X-Workspace-ID` or 400; `_SELF_SERVICE_SCOPES` and the 400 for a name outside it; the `["read"]` default; a session whose surface is in `PRIMA_SURFACES` is refused with 403 unless the resolved scopes are exactly `["ai"]`; the twelve-character prefix; `DELETE /keys/{id}` and its 404)alchemy_labs/backend/api/dependencies.py (`_GATEWAY_SCOPES`: an `ai`-scoped key is confined to `/api/prima/v1/` and answers 403 on every other path, `/api/developer/keys` included)alchemy_labs/backend/core/surfaces.py (`PRIMA_SURFACES`: `prima_web`, `prima_chat`, `prima_dev`)alchemy_labs/backend/services/primaapi/identity.py (`resolve_gateway_identity`: on the billing paths a key needs `ai` or one of `admin`, `full`, `*`; `_user_has_workspace_access`: the owner or any active `workspace_users` row, the read-side check; `user_may_change_billing` with `BILLING_ROLES = ("owner", "admin")`, the write-side predicate since 3d21356)alchemy_labs/backend/api/routers/primaapi_router.py, branch `w1-auto-topup` (`_require_labs_session`: 403 `session_required` to a key on `PUT` and `DELETE /credits/auto-topup` and on `save_card: true`; `_require_billing_role` after it since 3d21356: 403 `owner_required` on the same three writes and on every plain top-up)philosophers_stone/operations/decisions/0036-prepaid-credits-auto-topup-and-balance-events.md § 4, "Who may change the instruction" (amended (5)) and "Which member may change it" (amended (7), A-1: the role, the order of the guards, and `owner_required` as final for the user)philosophers_stone/operations/prima-onboarding-phase2-report.md § 5.A, the A-1 block (decided 2026-09-11: owner or admin only, 403 `owner_required` for any other member)alchemy_labs/backend/services/workspace/models.py (`workspace_users.role`: `owner`, `admin`, `member`, `viewer` documented on the column, `desk_requester` a real value below `viewer`) and alchemy_labs/backend/services/workspace/isolation.py (`verify_workspace_access`: the hierarchy the guard's predicate composes; `RBACService.is_workspace_admin` is the same membership test and is not what the guard calls, since it does not read `owner_id`)prima-chat/server/identity.ts (`POST /api/developer/keys` with `scopes: ['ai']`, one key per session)prima-developer/src/lib/gateway-key.ts, branch `w6-konto` @ d5f9aa8 (the portal's per-sign-in key: `scopes: ["ai"]` exactly, `expires_in_days: 1`, named after the session, revoked at sign-out)philosophers_stone/operations/decisions/0037-stone-per-tenant-forgejo-org-and-token.md (any X-API-Key on the stone endpoint is 403 `jwt_required`)