ZevAuth
Sign up

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