Apikee

FastAPI Example

A complete FastAPI Todo API protected with apikee.

FastAPI Example

A complete Todo API demonstrating SecuredFastAPI, ApikeeDepends, and scope enforcement.

Run it

pip install "apikee[fastapi]" uvicorn
uvicorn main:app --reload

Open http://localhost:8000/docs — every endpoint has a 🔒 lock.

Full source

from datetime import timedelta
from typing import Optional
from fastapi import Depends, Request
from fastapi.responses import JSONResponse
from apikee.fastapi import SecuredFastAPI, ApikeeDepends, require_scope
from apikee import ApikeeClaims

app = SecuredFastAPI(
    title="apikee Todo API",
    version="1.0.0",
    secrets=["local-dev-secret-change-in-production"],
    exclude_paths={"/health", "/docs", "/redoc", "/openapi.json", "/keys"},
    # server_key="sk_live_...",
    # project_env="my-app-production",
)

todos: dict[str, list[dict]] = {}


@app.get("/health", tags=["system"])
def health():
    return {"status": "ok"}


@app.post("/keys", tags=["auth"], summary="Issue a new API key")
def create_key(tenant: str, scopes: str = "read,write", plan: str = "free"):
    key = app.apikee.create(
        tenant=tenant,
        scopes=scopes.split(","),
        expires_in=timedelta(days=90),
        meta={"plan": plan},
    )
    return {"key": key, "tenant": tenant, "scopes": scopes.split(",")}


@app.get("/me", tags=["auth"])
def me(claims: ApikeeClaims = ApikeeDepends()):
    return {"id": claims.id, "tenant": claims.tenant, "scopes": claims.scopes}


@app.get("/todos", tags=["todos"])
def list_todos(claims: ApikeeClaims = ApikeeDepends()):
    return {"tenant": claims.tenant, "todos": todos.get(claims.tenant, [])}


@app.post("/todos", tags=["todos"])
def create_todo(
    title: str,
    description: Optional[str] = None,
    claims: ApikeeClaims = ApikeeDepends(),
):
    if "write" not in claims.scopes:
        return JSONResponse({"error": "INSUFFICIENT_SCOPE"}, 403)
    bucket = todos.setdefault(claims.tenant, [])
    todo = {"id": len(bucket) + 1, "title": title, "description": description, "done": False}
    bucket.append(todo)
    return todo


@app.delete("/todos/{todo_id}", tags=["todos"],
            dependencies=[Depends(require_scope("admin"))])
def delete_todo(todo_id: int, claims: ApikeeClaims = ApikeeDepends()):
    """Requires admin scope."""
    bucket = todos.get(claims.tenant, [])
    todos[claims.tenant] = [t for t in bucket if t["id"] != todo_id]
    return {"deleted": todo_id}

Try it

Issue a key

curl -X POST "http://localhost:8000/keys?tenant=acme&scopes=read,write"
export KEY="apikee_..."

Use the key

curl -H "x-api-key: $KEY" http://localhost:8000/todos
curl -X POST -H "x-api-key: $KEY" "http://localhost:8000/todos?title=Buy+milk"
curl -H "x-api-key: $KEY" http://localhost:8000/me

Test scope enforcement

# Issue read-only key
export RO_KEY=$(curl -s -X POST "http://localhost:8000/keys?tenant=bob&scopes=read" \
  | python3 -c "import sys,json; print(json.load(sys.stdin)['key'])")

# 403 — needs write
curl -X POST -H "x-api-key: $RO_KEY" "http://localhost:8000/todos?title=Fail"

# 403 — needs admin
curl -X DELETE -H "x-api-key: $KEY" http://localhost:8000/todos/1

On this page