Scopes
Naming scopes, enforcing them, and why they cannot change after issuance.
A scope is a short string naming one capability. Verification proves a key is genuine; scopes decide what that key may actually do.
read:users
write:orders
billing:refund
adminFormat
Scopes match ^[a-z0-9*][a-z0-9:._*-]{0,63}$ (case-insensitive), and a key may
carry at most 32 of them. Duplicates are collapsed.
The conventional shape is action:resource. Nothing enforces that — admin is a
perfectly valid scope — but a consistent convention pays off once you have more
than a handful.
| Pattern | Example | Use for |
|---|---|---|
action:resource | read:users | Most things |
resource:action:qualifier | orders:write:bulk | Finer splits |
| Bare capability | admin | Coarse, whole-surface access |
Enforcing
from apikee.fastapi import require_scope
from fastapi import Depends
@app.delete("/v1/users/{id}", dependencies=[Depends(require_scope("admin"))])
def delete_user(id: str): ...A missing scope is a 403, not a 401 — the caller is authenticated, just not
permitted.
Reading them yourself
The claims object always carries the full list, so you can branch on it directly when a decorator is too blunt:
if (req.apikee!.scopes.includes('billing:refund')) { /* … */ }Scopes are immutable
Scopes are signed into the key, so they cannot be edited afterwards. To change what a consumer may do, issue a new key and revoke the old one.
This is what makes local verification trustworthy: if scopes could drift server-side, every request would need a lookup to be sure, and the 0.1ms local path would be a lie.
Declaring scopes on endpoints
Registering which scopes a route needs does not enforce anything — enforcement is the code above. It populates your generated docs and the developer portal, so consumers can see what to ask for:
apikee.describeEndpoint('DELETE', '/v1/users/{id}', {
description: 'Permanently delete a user.',
scopes: ['admin'],
})See Endpoints.

