Apikee

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:write

Use 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  admin

Resource-action scopes — scales to larger APIs:

users:read   users:write   users:admin
orders:read  orders:write
billing:read

Tier-based scopes — map to pricing plans:

tier:free   tier:pro   tier:enterprise

Scopes 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.scopes

Scope validation error format

When a scope check fails, the middleware returns:

{
  "error":   "INSUFFICIENT_SCOPE",
  "message": "Key missing required scopes: admin"
}

HTTP status: 403 Forbidden.

On this page