Scopes & Access Control
Using scopes to implement role-based access control with apikee.
Scopes & Access Control
Scopes are strings embedded in the key that describe what the key is permitted to do. They are verified locally without any network call.
Defining scopes
Scopes are arbitrary strings — you define them for your API. Common patterns:
read write admin
users:read users:write users:admin
billing:read billing:writeUse whichever naming convention fits your API. Colon-namespacing (resource:action) scales well for complex APIs.
Embedding scopes in a key
key = apikee.create(
tenant="acme-corp",
scopes=["read", "billing:read"],
)const key = await apikee.create('acme-corp', {
scopes: ['read', 'billing:read'],
})engine.create(KeyOptions.builder()
.tenant("acme-corp")
.scopes(List.of("read", "billing:read"))
.build());engine.Create(new KeyOptions {
Tenant = "acme-corp",
Scopes = ["read", "billing:read"],
});Checking scopes in handlers
from apikee.fastapi import require_scope, ApikeeDepends
from fastapi import Depends
# Protect an entire route with a scope
@app.delete("/users/{id}", dependencies=[Depends(require_scope("admin"))])
def delete_user(id: str):
return {"deleted": id}
# Check manually inside a handler
@app.post("/orders")
def create_order(claims: ApikeeClaims = ApikeeDepends()):
if "write" not in claims.scopes:
raise HTTPException(403, "Requires write scope")
...from apikee.flask import require_scope
@app.delete("/users/<int:id>")
@require_scope("admin") # decorator form
def delete_user(id):
return {"deleted": id}
@app.post("/orders")
@require_scope("write")
def create_order():
...import { requireScope } from 'apikee/express'
// Middleware form
app.delete('/users/:id', requireScope('admin'), (req, res) => {
res.json({ deleted: req.params.id })
})
// Manual check
app.post('/orders', (req, res) => {
if (!req.apikee!.scopes.includes('write')) {
return res.status(403).json({ error: 'Requires write scope' })
}
})import { requireScope } from 'apikee/hono'
app.delete('/users/:id', requireScope('admin'), (c) =>
c.json({ deleted: c.req.param('id') })
)private void requireScope(ApikeeClaims claims, String scope) {
if (!claims.scopes().contains(scope)) {
throw new ResponseStatusException(FORBIDDEN,
"Required scope: " + scope);
}
}
@DeleteMapping("/users/{id}")
public ResponseEntity<?> deleteUser(@PathVariable Long id,
HttpServletRequest req) {
requireScope(claims(req), "admin");
// ...
}// Per-action attribute
[HttpDelete("{id}")]
[Apikee(Scopes = "admin")]
public IActionResult Delete(int id) { ... }
// Multiple scopes (AND — all required)
[Apikee(Scopes = "admin,write")]
public IActionResult SensitiveAction() { ... }Scope patterns
Flat scopes — simple, good for small APIs:
read write adminResource-action scopes — scales to larger APIs:
users:read users:write users:admin
orders:read orders:write
billing:readTier-based scopes — map to pricing plans:
tier:free tier:pro tier:enterpriseScopes are just strings — the SDK doesn't enforce any hierarchy. You implement your own scope logic. A key with admin doesn't automatically have write unless you code that relationship in your check.
Wildcard patterns (manual implementation)
If you want hierarchical scopes, implement the check yourself:
def has_scope(claims, required):
# exact match
if required in claims.scopes:
return True
# wildcard: "users:*" satisfies "users:read"
resource = required.split(":")[0]
return f"{resource}:*" in claims.scopes or "admin" in claims.scopesScope validation error format
When a scope check fails, the middleware returns:
{
"error": "INSUFFICIENT_SCOPE",
"message": "Key missing required scopes: admin"
}HTTP status: 403 Forbidden.

