Your backend
FastAPI
Protect FastAPI routes with dependencies, read the signed-in user, serve server-rendered pages and receive webhooks.
pip install "zevauth[fastapi]"
This page covers what FastAPI adds. The client, the backend API and the rules behind every refusal are on Python. It works the same in Starlette.
Setup
import os
from fastapi import FastAPI
from zevauth import ZevAuth
from zevauth.fastapi import ZevAuthFastAPI
zevauth = ZevAuth(
os.environ["ZEVAUTH_ENVIRONMENT_ID"],
secret_key=os.environ["ZEVAUTH_SECRET_KEY"],
)
za = ZevAuthFastAPI(zevauth)
app = FastAPI()
za.install(app)
install turns refusals into the ZevAuth error envelope and, when the
session bridge is on, mounts its routes.
Protecting routes
from fastapi import Depends
from zevauth import Auth
@app.get("/api/me")
async def me(auth: Auth = Depends(za.require_auth())):
return {"userId": auth.user_id}
@app.post("/api/payouts")
async def payout(auth: Auth = Depends(za.require_auth(require_mfa=True))):
...
@app.get("/api/mfa/setup")
async def setup(auth: Auth = Depends(za.require_auth(allow_pending_tasks=True))):
...
A refused request answers with the same JSON every ZevAuth library uses:
{ "error": { "code": "unauthorized", "message": "Sign in to continue." } }
The codes are unauthorized (401), session_task_pending (403, with
pendingTasks) and mfa_required (403).
Who is signed in, without refusing
@app.get("/")
async def home(auth: Auth = Depends(za.auth)):
return {"signedIn": auth.is_signed_in}
za.auth never refuses. Verification runs without blocking the event
loop: keys are fetched with an async client and cached.
Server-rendered pages
Turn the session bridge on and install
mounts POST /zevauth/session and GET /zevauth/handshake:
zevauth = ZevAuth(
os.environ["ZEVAUTH_ENVIRONMENT_ID"],
publishable_key=os.environ["ZEVAUTH_PUBLISHABLE_KEY"],
session_bridge=True,
)
Pages that depend on za.auth or za.require_auth() then read the
session cookie. When it has expired, a page request is redirected through
the handshake, which refreshes it in the browser and returns; an API call
gets a 401 with session_refresh_required instead.
Point the browser SDK at the endpoint with
sessionBridge: { url: '/zevauth/session' }.
Webhooks
from fastapi import Request, Response
from zevauth import ZevAuthWebhookError, construct_webhook_event
@app.post("/webhooks/zevauth")
async def webhook(request: Request):
try:
event = construct_webhook_event(
await request.body(), # the raw bytes
request.headers.get("zevauth-signature"),
os.environ["ZEVAUTH_WEBHOOK_SECRET"],
)
except ZevAuthWebhookError:
return Response(status_code=400)
...
Read await request.body(), not a Pydantic model: the signature covers
the exact bytes.
Backend API
Use zevauth.api_async in async routes:
user = await zevauth.api_async.users.get(auth.user_id) Updated at, Sunday, November 1, 2026