---
title: "Python SDK for FastAPI, Django and Flask · camada"
description: "Install camada in FastAPI, Django, Flask or any ASGI or WSGI app in two lines: the options it takes, what this position sees, and how it fails open."
url: https://www.camada.app/docs/python
last_updated: 2026-10-26
---

Docs

# Python: FastAPI, Django, Flask

`camada` on PyPI enforces your snapshot inside a Python process: your ordered custom rules, then allow, block and challenge, decided in-process, with events shipped off the request path. One package, with an ASGI middleware, a WSGI middleware, and integrations for FastAPI, Django and Flask.

## Install

shell

```sh
pip install camada
```

Python 3.10 or newer. No runtime dependencies.

## Mount it

FastAPI / Starlette

```python
from fastapi import FastAPI
from camada.fastapi import CamadaMiddleware

app = FastAPI()
app.add_middleware(CamadaMiddleware)   # key, ingest and snapshot URLs come from the environment
```

Django

```python
# settings.py
MIDDLEWARE = [
    "camada.django.CamadaMiddleware",   # first, so it answers before anything else runs
    *MIDDLEWARE,
]
```

Flask

```python
from flask import Flask
from camada.flask import init_app

app = Flask(__name__)
init_app(app)   # wraps app.wsgi_app, so camada answers before routing
```

any ASGI or WSGI app

```python
from camada.asgi import CamadaASGI   # any ASGI 3 app
from camada.wsgi import CamadaWSGI   # any WSGI app

application = CamadaASGI(application)
```

The engine is built from the environment on the first request it sees:

shell

```sh
export CAMADA_KEY=<ingest_token>.<snap_token>
export CAMADA_INGEST_URL=http://localhost:8787   # dev only; defaults to production ingest
```

Set these as environment variables: export them in the shell that starts your app, or add them in your host's settings. Python reads no .env file on its own.

An app that reads its own configuration can build the engine itself and hand it to the integration:

explicit config

```python
from camada import Camada
from camada.fastapi import CamadaMiddleware

engine = Camada(env={"CAMADA_KEY": MY_KEY, "CAMADA_INGEST_URL": MY_INGEST}, challenge_path="/api/__camada/challenge")
app.add_middleware(CamadaMiddleware, engine=engine)
```

Quickstarts: [FastAPI](https://www.camada.app/docs/quickstart/fastapi), [Django](https://www.camada.app/docs/quickstart/django), [Flask](https://www.camada.app/docs/quickstart/flask).

## The options you will use

| Option | Default | Meaning |
| --- | --- | --- |
| `env` | `os.environ` | Where CAMADA_KEY and friends are read from. Pass a mapping to configure in code. |
| `transport` | `urllib` | The HTTP callable that reaches the analyst; tests hand in an in-process fake. |
| `refresh_s` | `server-steered` | Snapshot poll cadence in seconds. Unset, the tenant config's poll_seconds steers it; set, it is pinned. |
| `challenge` | `True` | Serve the proof-of-work page for challenge verdicts (CAMADA_CHALLENGE=0 switches it off too). |
| `challenge_path` | `/__camada/challenge` | Where that page posts its solution. |
| `snapshot_version` | `5` | 4 drops your ordered custom rules; 3 the allow and challenge sections too. |
| `script_path` | `/_cam/b.js` | Where the first-party beacon script is served. |
| `fp_path` | `/_cam/fp` | Where that script posts the beacon. Keep it in the same directory as script_path: the script derives one from the other. |

Environment: `CAMADA_KEY`, `CAMADA_INGEST_URL`, `CAMADA_SNAPSHOT_URL`, `CAMADA_TRUSTED_PROXY`, `CAMADA_SERVERLESS=1` (lazy refresh, no poll thread), `CAMADA_CHALLENGE=0`, and the kill switch `CAMADA_DISABLED=1`, checked per request. The [docs index](https://www.camada.app/docs) lists what each one means.

## What it does per request

1. Keeps the snapshot fresh off the request path: a daemon thread polls at the cadence your tenant config sets (gzip on the wire, 304 when nothing changed), and asks for snapshot v5, the container that carries your ordered custom rules. `CAMADA_SERVERLESS=1` refreshes lazily instead.
2. Resolves the client from the socket peer, combined with `X-Forwarded-For` only under your trusted-proxy setting. A raw forwarded header is never trusted without one.
3. Runs your ordered custom rules, then the allow, block and challenge lists. A blocked request gets **403** with `x-block-reason` (and `x-block-rule` when a rule decided) before your app runs. The event still ships, marked as an SDK block so it is not confused with your own 403s.
4. A challenged browser gets the proof-of-work page; an API call reaches your app, and its response carries `x-camada-challenge: 1` for your frontend to act on. A request whose IP cannot be resolved is let through. See [challenge](https://www.camada.app/docs/challenge).
5. Serves the beacon endpoints, then runs your app with `x-rid` and the `_sfp` session cookie on its response, and ships one batched, redacted event with its real status and latency when the response is done. An exception in your app ships as `st: 500` and propagates unchanged.

## Add the beacon

Bots that never run JavaScript are the cheapest to catch. Put the tag in the `<head>` of the pages you render and the middleware does the rest:

beacon

```python
from camada.fastapi import script_tag

@app.get("/", response_class=HTMLResponse)
async def home(request: Request):
    return f"<html><head>{script_tag(request)}</head><body>…</body></html>"
```

`script_tag(request)` (Flask: `script_tag()`) returns a `<script src="/_cam/b.js?r=…" async>` tag carrying this request's event id, so the beacon is joined to the page view it came from. The middleware serves the script at `GET /_cam/b.js` (cacheable for an hour) and relays `POST /_cam/fp` onto the event batch, stamped with the client address camada resolved — never the one the body claims. Both endpoints sit behind the verdict, so a blocked client gets 403 there too. Turn the beacon off in the project's settings and the tag becomes empty and both endpoints fall through to your app.

## Report what your app knows

The wire shows a `POST /login`; only your handler knows whether it failed. Tell camada from the handler:

outcomes

```python
from camada.fastapi import track

@app.post("/login")
async def login(request: Request, form: LoginForm):
    ok = await sign_in(form)
    if not ok:
        track(request, "login_failed", user=form.email)   # hashed in-process; never raises
    return RedirectResponse("/") if ok else JSONResponse({"error": "invalid"}, 401)
```

`track(request, event, user=…)` (Django the same; Flask `track(event, user=…)`) ships the outcome joined to this request's event and session. The user identifier is hashed in-process with your ingest token before it is queued — the raw value never leaves the process. It never raises. The event name is free-form; the analyst's rules read this vocabulary:

| Event | When |
| --- | --- |
| `login_failed / login_succeeded` | a credential check settled |
| `signup` | an account was created |
| `password_reset` | a reset was requested |
| `mfa_failed` | a second factor was rejected |
| `payment_failed / payment_succeeded` | a charge settled |
| `coupon_failed` | a promo code was rejected |

A route you want to gate yourself can serve the challenge on demand:

gating a route

```python
from camada.fastapi import serve_challenge   # Django: camada.django, Flask: camada.flask

@app.get("/export")
async def export(request: Request):
    if answer := serve_challenge(request):
        return answer          # the proof-of-work page (or 403 JSON) until this browser holds a valid _cch
    return big_file()
```

## What this position sees

The same position as `@camada/node`, in a Python process: the final status and latency, the session, the beacon's browser signals and your app's outcomes. Under ASGI (FastAPI, Starlette, async Django) the event also carries the wire's header order; WSGI (Flask, sync Django) loses it, which is why the plan calls ASGI the first-class tier — camada knows what this tap can see from the position itself and never scores the absence of header order as evidence against a request. No ASN, country or TLS fingerprint reaches a Python process, so those conditions fail open in-app and the analyst resolves ASN and country itself. What a rule can key on is on [rules and list entries](https://www.camada.app/docs/rules).

## Deploying it

- Every worker process polls its own snapshot (about 5 MB resident each) and ships its own batches; the tenant's `poll_seconds` keeps the cadence honest across a fleet.
- Threads do not survive a fork. Under gunicorn `--preload` or uWSGI the SDK notices the fork and restarts its poll and flush threads in each worker.
- Pending events drain at interpreter exit within half a second. The SDK installs no signal handlers — an app owns its own shutdown — so a worker killed by SIGTERM without one may drop its last batch.
- Serverless hosts: `CAMADA_SERVERLESS=1`. A cold invocation fails open and catches up on the next request.

## It fails open

Without a key the middleware is **inert**: one log line, no requests, no enforcement, so an unprovisioned environment behaves exactly as if camada were not installed. Beyond that, a dead ingest, a corrupt snapshot or a bug in the package costs you the telemetry, never the request — camada can never turn one of your responses into a 5xx. The whole stance is on the [security](https://www.camada.app/security) page.

## Sitemap

[Every page on this site](https://www.camada.app/sitemap.md)
