---
title: FastAPI
description: Protect FastAPI routes with dependencies, read the signed-in user, serve server-rendered pages and receive webhooks.
---

```bash
pip install "zevauth[fastapi]"
```

This page covers what FastAPI adds. The client, the backend API and the
rules behind every refusal are on [Python](/sdk/python/). It works the
same in Starlette.

## Setup

```python
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

```python
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:

```json
{ "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

```python
@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](/concepts/session-bridge/) on and `install`
mounts `POST /zevauth/session` and `GET /zevauth/handshake`:

```python
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

```python
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:

```python
user = await zevauth.api_async.users.get(auth.user_id)
```
