API Routes
Plugins can expose HTTP endpoints via FastAPI routers. This guide covers creating routes, authentication, and accessing shared state.
See also: Backend Hooks, Security
Creating API Routes
Return a FastAPI APIRouter from the api_router() method on your plugin class:
def api_router(self):
from fastapi import APIRouter, Request
router = APIRouter(tags=["my_plugin"])
@router.post("/action")
async def do_action(request: Request):
return {"result": "success"}
return router
Routes are mounted at /ui/plugins/{plugin_id}/. For example, a plugin with plugin_id = "my_plugin" that defines a /action route will be accessible at:
POST /ui/plugins/my_plugin/action
Authentication (Automatic)
All plugin routes are automatically protected — no code required from plugin developers.
The system injects authentication middleware that:
- Requires authentication — Every request must have a valid session
- Verifies conversation ownership — If
{conversation_id}is in the route path, the authenticated user must own that conversation
Route Pattern Behavior
| Route Pattern | Behavior |
|---|---|
/votes/{conversation_id} | Auth required + ownership verified |
/stats/all | Auth required only |
Accessing the User
The authenticated user is available via request.state, injected by the auth middleware:
@router.get("/data/{conversation_id}")
async def get_data(conversation_id: str, request: Request):
# User is available via request.state (injected by auth middleware)
user = request.state.user
print(f"Request from user: {user.id}")
# conversation_id ownership is already verified by middleware
# No need to check manually
return {"user_id": user.id, "conversation_id": conversation_id}
Security Notes
- Routes without
conversation_idrequire authentication but allow any logged-in user - For admin-only routes, add explicit role checks in your handler
- Returns 404 (not 403) for unauthorized conversation access to prevent ID enumeration
Accessing Shared State
Use request.app.state for lazy-initialized shared stores:
@router.get("/data")
async def get_data(request: Request):
# Lazy-init stores via app state
store = getattr(request.app.state, "my_store", None)
if store is None:
store = MyStore()
request.app.state.my_store = store
return store.get_all()
This pattern ensures your store is created once and shared across all requests to your plugin.