# How to Add Authentication to a Python Backend - Part 3

> Part 3 of 3. Start with [How to Add Authentication to a Python Backend](https://clerk.com/articles/how-to-add-authentication-to-a-python-backend.md).

This is the final part of our guide on adding authentication to a Python backend. In Part 1, we covered the foundational concepts and setting up Clerk. In Part 2, we built concrete implementations using FastAPI and Flask. Now, in Part 3, we connect our Python API to a React frontend, explore advanced Clerk SDK features like webhooks and organizations, prepare for production deployment, and compare Clerk against other Python authentication options.

## Integrating with a React frontend

Python doesn't render sign-in UI. React (or Next.js) is the most common frontend pairing. Here's the handshake.

### Minimal React setup

```bash
npm install @clerk/react
```

Wrap your app:

```tsx
import { ClerkProvider, Show, SignIn, UserButton } from '@clerk/react'

const publishableKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY

export default function App() {
  return (
    <ClerkProvider publishableKey={publishableKey}>
      <Show when="signed-out">
        <SignIn />
      </Show>
      <Show when="signed-in">
        <UserButton />
        <ProtectedPage />
      </Show>
    </ClerkProvider>
  )
}
```

`<Show when="...">` is Clerk's current auth-state gate — it replaces the older `<SignedIn>` / `<SignedOut>` (and `<Protect>`) components, which are deprecated in Clerk's Core 3 SDKs (`@clerk/react`).

That's the whole frontend prerequisite. Full setup: [Clerk React quickstart](https://clerk.com/docs/react/getting-started/quickstart.md).

### Calling your Python API from React

The pattern that does the work:

```tsx
import { useAuth } from '@clerk/react'

function ProtectedPage() {
  const { getToken } = useAuth()

  async function fetchMe() {
    const token = await getToken()
    const res = await fetch('http://localhost:8000/api/me', {
      headers: { Authorization: `Bearer ${token}` },
    })
    return res.json()
  }

  return <button onClick={fetchMe}>Load profile</button>
}
```

`useAuth().getToken()` returns the short-lived (60-second) session JWT. The SDK auto-refreshes roughly every 50 seconds, so a long-lived page stays authenticated as long as the Clerk session is valid. Force a fresh token with `getToken({ skipCache: true })` if you're debugging expiry behavior.

### CORS on the Python side

If your frontend and Python backend live on different origins (`localhost:5173` and `localhost:8000` during development, or `app.example.com` and `api.example.com` in production), [CORS](https://fastapi.tiangolo.com/tutorial/cors/) has to allow the request. The React example above uses [Vite](https://vite.dev/config/server-options), which serves on port 5173; Next.js and Create React App default to 3000, so match `allow_origins` to whatever URL your dev server prints on startup.

FastAPI:

```python
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173", "http://localhost:3000", "https://yourapp.com"],
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "DELETE"],
    allow_headers=["Authorization", "Content-Type"],
)
```

Flask, via the [`flask-cors`](https://pypi.org/project/flask-cors/) extension (`uv add flask-cors`):

```python
from flask_cors import CORS

CORS(
    app,
    resources={r"/api/*": {"origins": ["http://localhost:5173", "http://localhost:3000", "https://yourapp.com"]}},
    supports_credentials=True,
)
```

The gotcha: if you enable credentials, you cannot use `"*"` for origins. The browser rejects the response. List origins explicitly. For bearer-token auth (`Authorization: Bearer ...`), you don't need credentials mode — that's only for cookies.

Your `CORS_ORIGINS` should also appear in `CLERK_AUTHORIZED_PARTIES`. The checks serve different layers (CORS protects the browser; `azp` protects the token), but they must be consistent.

## Advanced Clerk SDK features for Python backends

Once the basic auth loop works, these are the SDK corners most teams end up in.

### User metadata access

Clerk users have three metadata buckets:

1. `public_metadata` — readable from frontend and backend. Use for feature flags, subscription tiers, anything safe to ship to the browser.
2. `unsafe_metadata` — writable from the frontend SDK. Use for user-controlled preferences.
3. `private_metadata` — backend-only. Use for sensitive flags, internal IDs, things you don't want the user to see or change.

Reading and updating:

```python
clerk = Clerk(bearer_auth=os.environ["CLERK_SECRET_KEY"])

user = clerk.users.get(user_id="user_xxx")
print(user.public_metadata)  # {"subscription_tier": "pro"}

clerk.users.update_metadata(
    user_id="user_xxx",
    public_metadata={"subscription_tier": "enterprise"},
)
```

Metadata has its own dedicated methods — `update_metadata()` merges (patch) and `replace_metadata()` overwrites (added in SDK v6.0.0). The general `clerk.users.update()` no longer accepts metadata fields as of the 2026-05-12 API version, so reach for these instead.

Common use case: Stripe webhook handler updates `public_metadata.subscription_tier` after a successful checkout. Frontend reads it from `useUser().user.publicMetadata` and gates premium features accordingly. No separate database needed for this data.

### Session management

List active sessions, revoke one, or revoke all. "Sign out everywhere":

```python
sessions = []
offset = 0
while True:
    page = clerk.sessions.list(user_id="user_xxx", status="active", limit=100, offset=offset)
    if not page:
        break
    sessions.extend(page)
    offset += 100

for session in sessions:
    clerk.sessions.revoke(session_id=session.id)
```

Mind the pagination: `sessions.list()` defaults to `limit=10`, so a bare call quietly misses sessions for users signed in on many devices. Collect every page first, then revoke.

Revoking a session marks it `revoked` server-side. Clerk issues no long-lived refresh token — just short 60-second JWTs — so the user is signed out within about a minute, on the next token refresh. The async equivalent is `clerk.sessions.list_async(...)`.

### Organizations and team features

Clerk [organizations](https://clerk.com/glossary.md#organizations) provide B2B [multi-tenancy](https://clerk.com/glossary/multi-tenancy.md). The Backend API mirrors the frontend SDK:

```python
# Create an org
org = clerk.organizations.create(request={"name": "Acme Inc", "slug": "acme"})

# List a user's orgs
memberships = clerk.users.get_organization_memberships(user_id="user_xxx")

# Invite a member
clerk.organization_invitations.create(
    organization_id=org.id,
    email_address="teammate@example.com",
    role="org:admin",
)
```

Roles and permissions management shipped on [2025-11-24](https://clerk.com/changelog/2025-11-24-organization-roles-and-permission-bapi-management.md). Create custom roles programmatically instead of only via the Dashboard:

```python
role = clerk.organization_roles.create(
    name="Analyst",
    key="org:analyst",
    description="Read-only access to reports",
)
```

### Webhook handling in Python

[Webhooks](https://clerk.com/glossary.md#webhook) are how Clerk tells your Python backend about user events — `user.created`, `user.updated`, `user.deleted`, organization events, role changes. Sync them into your database, trigger emails, update analytics, whatever.

Clerk delivers webhooks via [Svix](https://www.svix.com/). You verify the signature with Svix's Python SDK.

```bash
uv add svix
```

FastAPI handler:

```python
from fastapi import APIRouter, HTTPException, Request
from svix.webhooks import Webhook, WebhookVerificationError

from app.config import get_settings

router = APIRouter()

@router.post("/webhooks/clerk")
async def clerk_webhook(request: Request):
    body = await request.body()
    headers = dict(request.headers)
    settings = get_settings()
    try:
        event = Webhook(settings.clerk_webhook_signing_secret).verify(body, headers)
    except WebhookVerificationError:
        raise HTTPException(status_code=400, detail="invalid signature")
    event_type = event["type"]
    data = event["data"]
    if event_type == "user.created":
        await handle_user_created(data)
    elif event_type == "user.deleted":
        await handle_user_deleted(data["id"])
    return {"ok": True}
```

Flask handler — same shape, different framework primitives:

```python
from flask import Blueprint, abort, current_app, request
from svix.webhooks import Webhook, WebhookVerificationError

bp = Blueprint("webhooks", __name__)


@bp.post("/webhooks/clerk")
def clerk_webhook():
    body = request.get_data()
    headers = dict(request.headers)
    try:
        event = Webhook(
            current_app.config["CLERK_WEBHOOK_SIGNING_SECRET"]
        ).verify(body, headers)
    except WebhookVerificationError:
        abort(400, description="invalid signature")
    if event["type"] == "user.created":
        handle_user_created(event["data"])
    return {"ok": True}
```

Details to get right:

**Use the raw body.** Signature verification is an HMAC over raw bytes. If FastAPI/Flask parses the JSON, the HMAC won't match. `await request.body()` and `request.get_data()` return raw bytes.

**Required headers.** `Webhook.verify()` needs `svix-id`, `svix-timestamp`, and `svix-signature` (or their `webhook-*` aliases). They're never optional.

**Casing is handled.** The Svix SDK lowercases headers on entry. `dict(request.headers)` is safe to pass in.

**Reverse-proxy gotcha.** If your webhook endpoint sits behind Nginx, Cloudflare, or an API gateway, make sure the `svix-*` (or `webhook-*`) headers pass through untouched. Some WAFs strip unrecognized headers. If `Webhook.verify()` throws `WebhookHeadersError`, log the header keys (not values) to confirm the proxy isn't dropping them.

**Replay prevention.** Svix ships with a [built-in 5-minute timestamp window](https://www.svix.com/resources/webhook-best-practices/security/). Requests older than that fail verification automatically.

**Idempotency.** Svix guarantees at-least-once delivery, so you will see the same event twice occasionally. Persist processed `svix-id` values for about 24 hours and acknowledge duplicates with a `2xx` — a non-2xx response just triggers more retries. The `user.created` handler below gets the same effect more simply by checking for an existing `clerk_id` before inserting.

**The webhook endpoint must bypass auth.** Don't put `@clerk_required` or global auth middleware in front of it. Svix signs with a different secret than session tokens; `authenticate_request()` on the same request would reject it.

**Test locally before deploying.** Clerk delivers webhooks over the public internet, so expose your local server with a tunnel — Clerk's guide uses [ngrok](https://ngrok.com) (`ngrok http 8000`), and [localtunnel, Cloudflare Tunnel, or Pinggy](https://clerk.com/docs/guides/development/webhooks/debugging.md) work too — then register the forwarding URL plus your route (`https://<id>.ngrok-free.app/webhooks/clerk`) as the endpoint on the [Webhooks page](https://dashboard.clerk.com/~/webhooks) in the Clerk Dashboard and copy that endpoint's signing secret (each endpoint has its own) into `CLERK_WEBHOOK_SIGNING_SECRET`. To fire a payload without creating real data, open the endpoint's **Testing** tab, pick an event like `user.created`, and click **Send Example**. The [Svix CLI](https://www.svix.com/guides/testing-webhooks-locally-svix-cli/) is a no-Dashboard alternative: `svix listen http://localhost:8000/webhooks/clerk` prints a public URL and relays every delivery to your handler.

**Store Clerk's `id` as a foreign key, not a primary key.** Keep your own integer primary keys; make `clerk_id` a unique, indexed, non-null string column. This is what Clerk's [syncing guide](https://clerk.com/docs/guides/development/webhooks/syncing.md) recommends.

A minimal SQLAlchemy 2.0 `User` model to back the webhook handler:

```python
from sqlalchemy import String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column


class Base(DeclarativeBase):
    pass


class User(Base):
    __tablename__ = "users"
    id: Mapped[int] = mapped_column(primary_key=True)
    clerk_id: Mapped[str] = mapped_column(String(255), unique=True, index=True)
    email: Mapped[str | None] = mapped_column(String(320))
    subscription_tier: Mapped[str] = mapped_column(String(32), default="free")
```

About `String(255)`. Clerk user IDs are currently 32 characters (`user_` + 27 word chars per the [OpenAPI spec](https://github.com/clerk/openapi-specs) pattern `^user_\w{27}$`), but the schema component for `User.id` is plain `type: string` with no `maxLength` or `pattern` constraint. The Python SDK types it as plain `str`. Clerk's own public-API fields that carry user IDs use `maxLength: 255`. PostgreSQL stores `VARCHAR(n)` and `VARCHAR(255)` identically on disk. Tightening to `String(32)` saves nothing and creates a silent-failure risk if Clerk ever lengthens the ID format. Keep `String(255)`.

The `user.created` handler:

```python
async def handle_user_created(data: dict):
    clerk_id = data["id"]
    email = None
    for address in data.get("email_addresses") or []:
        if address["id"] == data.get("primary_email_address_id"):
            email = address["email_address"]
            break
    async with async_session() as session:
        existing = await session.scalar(
            select(User).where(User.clerk_id == clerk_id)
        )
        if existing:
            return  # duplicate delivery — already processed, acknowledge with 2xx
        session.add(User(clerk_id=clerk_id, email=email))
        await session.commit()
```

The `user.deleted` handler is symmetric:

```python
async def handle_user_deleted(clerk_id: str):
    async with async_session() as session:
        user = await session.scalar(
            select(User).where(User.clerk_id == clerk_id)
        )
        if user:
            await session.delete(user)
            await session.commit()
```

Source: [Svix receiving webhooks with FastAPI](https://www.svix.com/guides/receiving/receive-webhooks-with-python-fastapi/), [Svix receiving with Flask](https://www.svix.com/guides/receiving/receive-webhooks-with-python-flask/), [Svix verification internals](https://docs.svix.com/receiving/verifying-payloads/how).

### User impersonation for support

Support workflows often need impersonation. Clerk calls this **actor tokens**. The Python SDK [exposes `clerk.actor_tokens.create(...)`](https://raw.githubusercontent.com/clerk/clerk-sdk-python/main/src/clerk_backend_api/actortokens.py). It takes two mandatory fields: the user (`user_id`) and the admin (`actor.sub`). The admin is embedded in the session's `act` claim for the audit trail.

```python
actor_token = clerk.actor_tokens.create(request={
    "user_id": "user_target_xxx",             # user being impersonated
    "actor": {"sub": "user_admin_xxx"},        # admin doing the impersonating (ends up in `act`)
    "expires_in_seconds": 3600,                # optional, default 1 hour
    "session_max_duration_in_seconds": 1800,   # optional, default 30 minutes
})

# Share the one-time sign-in URL with the support agent.
# Visiting it signs them in as the target user; the resulting session
# carries `act.sub = "user_admin_xxx"` so your audit logs track it back.
print(actor_token.url)
print(actor_token.token)  # raw ticket value if you want to build the URL yourself
```

Revoke before expiry with `clerk.actor_tokens.revoke(actor_token_id=actor_token.id)`.

Do **not** confuse `actor_tokens` with `sign_in_tokens` — the latter is a one-time sign-in link for a normal user (no `act` claim, no audit trail), not an impersonation primitive. Full guide: [user impersonation](https://clerk.com/docs/guides/users/impersonation.md).

### Machine-to-machine authentication

When a service calls another service (not on behalf of a user), you want M2M tokens. Clerk supports two formats:

1. **Opaque** (`m2m_xxx`) — revocable. Best when you want to invalidate a token immediately.
2. **JWT** (`mt_xxx`) — a signed JWT, not revocable until expiry. Shipped [2026-02-24](https://clerk.com/changelog/2026-02-24-m2m-jwt-tokens.md).

One Python-specific caveat: the current Python SDK verifies _both_ formats with a call to Clerk's API — `verify_token` routes every machine token through network verification, and passing `jwt_key` doesn't change that path (it only affects session tokens). The networkless upside of the JWT format applies to SDKs that verify machine JWTs locally; in Python, plan for a network round-trip either way.

Restrict an endpoint to M2M traffic only:

```python
state = authenticate_request(
    request,
    AuthenticateRequestOptions(
        secret_key=settings.clerk_secret_key,
        # "machine_token" accepts opaque m2m_ tokens; "m2m_token" accepts JWT mt_ tokens
        accepts_token=["machine_token", "m2m_token"],
    ),
)
```

The `accepts_token` values don't match the prefixes one-to-one: `machine_token` is the opaque `m2m_` format and `m2m_token` is the JWT `mt_` format. List both to accept either while still rejecting session tokens, or just one to lock the endpoint to a single format.

API Keys (user-scoped programmatic access, prefix `ak_`) went [GA on 2026-04-17](https://clerk.com/changelog/2026-04-17-api-keys-ga.md). If you want both session tokens and API keys to work on the same endpoint, pass `accepts_token=["session_token", "api_key"]`. Full guide: [machine auth overview](https://clerk.com/docs/guides/development/machine-auth/overview.md).

## Production deployment considerations

These break on the first 2 a.m. page if you skip them.

### Secret management

Never commit `.env`. `.gitignore` it and commit an `.env.example`.

Rotation plan for `CLERK_SECRET_KEY`: create a new secret key in the Dashboard, deploy it to production, wait one deploy cycle, delete the old one. Clerk accepts both during the window. Rotate annually or on suspected compromise. Keep `CLERK_JWT_KEY` (public) separate from `CLERK_SECRET_KEY` (private) in your secret store; the JWT key can live in plain environment config, but the secret key should be in a proper secret manager.

Options: AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault, Doppler, Infisical. Railway, Render, Fly.io, and Vercel all have encrypted-at-rest environment variable storage that's sufficient for small teams. Pick whichever fits your platform; the pattern (inject at boot, never log) is the same.

### CORS and authorized parties

Repeat of the CORS gotcha from the React-integration section above: `allow_origins=["*"]` + `allow_credentials=True` breaks every browser. List origins explicitly.

`authorized_parties` on `AuthenticateRequestOptions` is your CSRF backstop. Even if CORS is misconfigured, `azp` validation rejects tokens issued for origins you didn't list. It's a defense-in-depth check, not a substitute for CORS.

Subdomain strategies:

1. **Same-apex** (`app.example.com` + `api.example.com`) — same-site cookies work if you set the cookie domain to `.example.com`. [SameSite=Lax](https://clerk.com/glossary.md#cookie-attributes) is fine for most flows.
2. **Cross-origin** (`yourapp.com` + `api.someothercompany.com`) — you're in full cross-origin territory. Use `Authorization: Bearer ...`, don't bother with cookies, and be strict about CORS origins.

### Performance and caching

Networkless verification with `jwt_key` is one PEM decode per process, cached in memory. Each subsequent request is a local RS256 signature check — no network round-trip to Clerk.

Networked verification fetches JWKS from `https://api.clerk.com/v1/jwks` once per process per `kid`. Cached in memory. Re-fetches on signature mismatch (key rotation). The first request after startup pays a one-time network round-trip; every request after that is a local signature check against the cached key.

Don't call `sdk.users.get()` on every request. The session token already has `sub`, `sid`, and the org claims. Only fetch the full user when you need metadata you can't get from the token.

Reuse a single `Clerk()` instance at module scope. Reusing the underlying `httpx` client allows HTTP connection pooling and avoids handshaking for every request. Creating a new `Clerk()` per request is an anti-pattern.

### Observability and logging

Log per request: timestamp, `user_id`, `session_id`, request ID, endpoint, outcome, latency, rejection reason.

What to never log: the full token, the raw `Authorization` header, password hashes, PII like email addresses unless you have a reason.

Structured logging with [`structlog`](https://www.structlog.org/) (preferred) or [`loguru`](https://github.com/Delgan/loguru) (popular but set `diagnose=False` in production — it can leak secrets into tracebacks).

OpenTelemetry instrumentation for FastAPI and Flask: `opentelemetry-instrumentation-fastapi`, `opentelemetry-instrumentation-flask`, and `opentelemetry-instrumentation-httpx` (to trace outbound Clerk Backend API calls). Set `OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS=authorization,cookie,svix-signature` so the tracing layer doesn't exfiltrate credentials to your APM.

### Deployment platforms

Short rundowns of where Python APIs actually run in 2026.

**Railway** — `railway.json` or `railway.toml` for config; `startCommand` for the worker command. Railway's [Railpack builder defaults to Python 3.13](https://railpack.com/languages/python); pin a different version with `.python-version`.

**Render** — `uvicorn app.main:app --host 0.0.0.0 --port $PORT` for FastAPI or `gunicorn "app:create_app()" -b 0.0.0.0:$PORT` for Flask. [Free tier sleeps after 15 minutes idle](https://render.com/docs/free); the [Starter plan is $7/month](https://render.com/pricing) for always-on.

**Fly.io** — `fly secrets set CLERK_SECRET_KEY=...` writes to an encrypted vault; the API servers [can only encrypt, not decrypt, stored secret values](https://fly.io/docs/apps/secrets/). Python docs at [fly.io/docs/python](https://fly.io/docs/python/).

**Heroku** — `Procfile`: `web: gunicorn -k uvicorn_worker.UvicornWorker app.main:app`. Newly created Python apps [default to the latest patch of Python 3.14](https://devcenter.heroku.com/articles/python-support); pin explicitly via `.python-version`.

**Google Cloud Run** — Dockerfile with `gunicorn` on `$PORT`; `--set-env-vars` or pull from Secret Manager with `roles/secretmanager.secretAccessor`. The [free tier includes 180,000 vCPU-seconds and 2 million requests per month](https://cloud.google.com/run/pricing) under request-based billing (240,000 vCPU-seconds under instance-based billing).

**AWS Lambda via Mangum** — `Mangum(app, lifespan="off")`. Initialize `Clerk()` and `httpx.Client` at module level so warm invocations reuse them. [Lambda SnapStart](https://docs.aws.amazon.com/lambda/latest/dg/snapstart.html) is available for Python 3.12 and later to reduce cold-start latency. [Mangum on PyPI](https://pypi.org/project/mangum/).

**Vercel Python Functions** — [Supports Python 3.12 (default), 3.13, and 3.14](https://vercel.com/docs/functions/runtimes/python). Auto-detects FastAPI and Flask from `requirements.txt`. Good fit when your frontend is already on Vercel.

Cold-start concerns matter on serverless (Lambda, Cloud Run, Vercel). Networkless `jwt_key` mode eliminates the JWKS warmup cost — one less thing to worry about on the first request after a scale-up.

### Frontend-backend communication in production

Same-origin deployment: use [Next.js 16's `proxy.ts`](https://nextjs.org/blog/next-16) to proxy `/api/*` internally to your Python service via `rewrites`. `proxy.ts` replaces the deprecated `middleware.ts` as the top-level file in Next.js 16 (released 2025-10-21). Clerk's import path is unchanged — the function is still `clerkMiddleware()` from `@clerk/nextjs/server`; only the file name changed. Next.js 15 and earlier projects keep the file as `middleware.ts`.

Cross-origin deployment (SPA at `yourapp.com`, Python API at `api.yourapp.com`): no file-rename concern. Rely on `authorized_parties` on the Python side and an explicit `CORSMiddleware` allow-list.

Cookie `SameSite=Lax` for same-origin; bearer tokens for cross-origin. Don't try to make cookies work across third-party domains — browsers are actively removing third-party cookie support and you'll spend weeks debugging.

### Clerk production instance checklist

- [ ] Switch to `pk_live_` / `sk_live_` in production env vars.
- [ ] Configure DNS CNAMEs per the Dashboard's instructions (your `clerk.yourapp.com` subdomain has to exist).
- [ ] Set up your own OAuth credentials with each provider (Google, GitHub, etc.). Clerk's shared dev OAuth apps don't work in production.
- [ ] Verify your `authorized_parties` list matches your real production origins.
- [ ] Turn off dev-only sign-in methods if you don't want them (e.g., test email codes).

Canonical checklist: [deploy to production](https://clerk.com/docs/guides/development/deployment/production.md).

## Clerk vs. other Python authentication options

The options at a glance. Pricing and free-tier numbers are verified against vendor pricing pages in April 2026; capability rows cite a primary vendor source.

| Capability                                                                          | Clerk                                                                                                                       | Auth0                                                                                                                         | Supabase Auth                                                                                                                              | Firebase Auth                                                                                                                 | AWS Cognito                                                                                                                                                               | DIY (PyJWT + pwdlib)                                                                           |
| ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| First-party Python SDK                                                              | `clerk-backend-api` 6.0.1 ([PyPI](https://pypi.org/project/clerk-backend-api/))                                             | `auth0-api-python` / `auth0-fastapi-api` ([Auth0 Python Quickstart](https://auth0.com/docs/quickstart/backend/python))        | Python server SDK via `supabase-py` ([docs](https://supabase.com/docs/reference/python/introduction))                                      | `firebase-admin` ([PyPI](https://pypi.org/project/firebase-admin/))                                                           | `boto3` (AWS SDK, generic)                                                                                                                                                | n/a                                                                                            |
| [Passkeys](https://clerk.com/glossary.md#passkeys) included in lowest paid tier     | Yes — Pro ([pricing](https://clerk.com/pricing))                                                                            | Yes — all tiers, including Free ([pricing](https://auth0.com/pricing))                                                        | Beta — native passkeys shipped May 2026 ([changelog](https://supabase.com/changelog/46458-passkeys-for-supabase-auth-beta))                | Not offered                                                                                                                   | Yes — Essentials ([pricing](https://aws.amazon.com/cognito/pricing/))                                                                                                     | DIY                                                                                            |
| MFA (TOTP) on free tier                                                             | Pro and above ([pricing](https://clerk.com/pricing))                                                                        | No — Essentials and above ([pricing](https://auth0.com/pricing))                                                              | Yes ([MFA docs](https://supabase.com/docs/guides/auth/auth-mfa))                                                                           | Blaze (pay-as-you-go) plan only ([pricing](https://firebase.google.com/pricing))                                              | Yes — Lite (free) tier ([pricing](https://aws.amazon.com/cognito/pricing/))                                                                                               | DIY                                                                                            |
| Native [organizations](https://clerk.com/glossary.md#organizations) / B2B primitive | Yes — first-class ([organizations docs](https://clerk.com/docs/guides/organizations/overview.md))                           | Yes — first-class ([Organizations](https://auth0.com/docs/manage-users/organizations))                                        | Not a first-class primitive — modeled via Postgres tables and [RLS](https://supabase.com/docs/guides/database/postgres/row-level-security) | Not offered                                                                                                                   | Groups (flat, not hierarchical) ([Cognito user groups](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-user-groups.html))                    | DIY                                                                                            |
| Webhooks for user events                                                            | Yes — via [Svix](https://www.svix.com/) ([webhook docs](https://clerk.com/docs/guides/development/webhooks/overview.md))    | Yes — Log Streams ([docs](https://auth0.com/docs/customize/log-streams))                                                      | Yes — auth hooks ([docs](https://supabase.com/docs/guides/auth/auth-hooks))                                                                | Cloud Functions [blocking triggers](https://firebase.google.com/docs/auth/extend-with-blocking-functions) (not HTTP webhooks) | Lambda Triggers ([docs](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html)) (not HTTP webhooks) | DIY                                                                                            |
| Local JWT verification with public key (no per-request network call)                | Yes — `jwt_key=` parameter ([manual verification guide](https://clerk.com/docs/guides/sessions/manual-jwt-verification.md)) | Yes — JWKS fetched and cached ([token validation](https://auth0.com/docs/secure/tokens/access-tokens/validate-access-tokens)) | Yes — JWKS / HS256 ([docs](https://supabase.com/docs/guides/auth/jwt-fields))                                                              | Yes — via `firebase-admin` [`verify_id_token`](https://firebase.google.com/docs/auth/admin/verify-id-tokens)                  | Yes — JWKS ([docs](https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html))                                | DIY                                                                                            |
| Free-tier unit and allowance                                                        | 50,000 MRUs ([pricing](https://clerk.com/pricing))                                                                          | 25,000 MAUs ([pricing](https://auth0.com/pricing))                                                                            | 50,000 MAUs ([pricing](https://supabase.com/pricing))                                                                                      | 50,000 MAUs Spark plan ([pricing](https://firebase.google.com/pricing))                                                       | 10,000 MAUs Lite ([pricing](https://aws.amazon.com/cognito/pricing/))                                                                                                     | Free                                                                                           |
| Starting paid tier                                                                  | $20/mo annual ($25 monthly) Pro ([pricing](https://clerk.com/pricing))                                                      | $35/mo Essentials ([pricing](https://auth0.com/pricing))                                                                      | $25/mo Pro ([pricing](https://supabase.com/pricing))                                                                                       | pay-as-you-go Blaze ([pricing](https://firebase.google.com/pricing))                                                          | $0.015/MAU Essentials ([pricing](https://aws.amazon.com/cognito/pricing/))                                                                                                | $50K–$200K/yr SOC 2 ([WorkOS guide](https://workos.com/blog/python-authentication-guide-2026)) |
| SOC 2 report access tier                                                            | Business ([pricing](https://clerk.com/pricing))                                                                             | "Compliance Certifications" included all tiers ([pricing](https://auth0.com/pricing))                                         | Team and Enterprise ([Supabase security](https://supabase.com/security))                                                                   | Available under Google Cloud ([SOC 2](https://cloud.google.com/security/compliance/soc-2))                                    | Available under AWS ([AWS Compliance](https://aws.amazon.com/compliance/soc-faqs/))                                                                                       | DIY                                                                                            |

Numbers sourced from each vendor's pricing and compliance page (links in table), the [Clerk "new plans, more value" changelog](https://clerk.com/changelog/2026-02-05-new-plans-more-value.md) (2026-02-05 repricing), and [WorkOS's 2026 guide](https://workos.com/blog/python-authentication-guide-2026) for the DIY cost range.

Clerk's tier detail, since numbers move:

1. **Hobby** (free) — 50,000 MRUs. Basic RBAC (20-member org cap), 5 impersonations/month, 3 dashboard seats, 7-day fixed sessions. Does not include MFA, passkeys, or Enterprise SSO.
2. **Pro** — $20/month billed annually ($25 monthly). Adds MFA, passkeys, custom email templates, custom session duration, SMS codes, satellite domains ($10/mo each), remove Clerk branding, and 1 Enterprise SSO connection included (additional connections from $75/mo, with volume discounts at higher counts).
3. **Business** — $250/month billed annually ($300 monthly). Adds SOC 2 Report access, HIPAA artifact access, 10 dashboard seats (additional $20/mo each), enhanced dashboard roles, and priority support.
4. **Enterprise** — custom pricing, annual only. Adds **HIPAA compliance available with BAA**, 99.99% uptime SLA, premium support, dedicated onboarding and migration support, custom Slack channel, and annual committed-use discounts.

> The **Business** plan includes access to the SOC 2 report and HIPAA artifacts (compliance documentation). **Signing a BAA for HIPAA-covered production workloads requires the Enterprise plan**, per Clerk's pricing page. Verify with Clerk's [pricing page](https://clerk.com/pricing) and the [2026-02-05 "new plans, more value" changelog](https://clerk.com/changelog/2026-02-05-new-plans-more-value.md) before making a compliance decision — these tiers shift.

### When to pick Clerk

1. You want a first-party Python SDK with predictable release cadence.
2. You need strong B2B / organizations support, including programmatic role and permission management.
3. You already use React, Next.js, or Expo — Clerk's frontend components match the backend one-to-one.
4. You want [passkeys](https://clerk.com/glossary.md#passkeys), MFA, and social OAuth without building them.
5. You need SOC 2 Report access on the Business tier, or HIPAA BAA availability on Enterprise, for compliance reasons.

### When another option might fit better

**Supabase** if your whole stack is Supabase — Postgres plus realtime plus storage plus auth, all consolidated. The client-SDK focus on auth means you'll write more Python verification code manually, but you get a tight DB integration.

**Firebase** if you're deep in Google Cloud — Firestore, Cloud Functions triggers, App Check integration. The organizations gap matters if your app is B2B.

**Auth0** if you need extensive enterprise SSO beyond what Clerk offers in Pro. Auth0's Enterprise tier has more granular SAML/OIDC controls. Pricing starts at $35/month for Essentials (25,000 MAU free on the base plan) and scales up as you add MAUs and enterprise features.

**Cognito** if you're AWS-native with no other preference. You'll write more Python yourself (no dedicated FastAPI/Flask helper), but Lambda Triggers compose well with the rest of your stack.

**DIY** only if authentication is literally your product (you're building an IDP), or for learning. The time and compliance cost is hard to justify otherwise.

None of these are bad choices. Clerk is the default recommendation for a modern Python API paired with a modern frontend; the others fit legitimate niches.

## Conclusion

Adding authentication to a Python backend doesn't have to mean reinventing the wheel with manual token validation or maintaining your own password reset flows. By leveraging Clerk's Python SDK, you can secure your FastAPI or Flask applications efficiently while offloading the complex frontend user management. From basic React integrations to advanced webhook handling, machine-to-machine auth, and production-ready deployments, you now have a comprehensive blueprint for securing your Python APIs.

## FAQ

## FAQ

### Where does Clerk look for the session token — cookie or header?

Both. `authenticate_request()` checks `Authorization: Bearer <token>` first, then the `__session` cookie as a fallback. Same-origin setups typically use the cookie; cross-origin SPAs and mobile clients use the header. The SDK handles either transparently.

### How do I handle Clerk webhooks in Python?

Install [`svix`](https://pypi.org/project/svix/), read the raw request body (`await request.body()` in FastAPI, `request.get_data()` in Flask), and call `Webhook(signing_secret).verify(body, headers)`. On success you get the parsed event — branch on `event["type"]` to handle `user.created`, `user.updated`, etc.

### Does Clerk support machine-to-machine (M2M) authentication in Python?

Yes. Pass `accepts_token=["machine_token", "m2m_token"]` into `AuthenticateRequestOptions` to restrict an endpoint to machine tokens — `machine_token` matches opaque `m2m_` tokens and `m2m_token` matches JWT `mt_` tokens. Clerk supports both formats: opaque (`m2m_`, revocable) and JWT (`mt_`, non-revocable until expiry); the Python SDK verifies both via Clerk's API. API keys (`ak_`, user-scoped) went [GA on 2026-04-17](https://clerk.com/changelog/2026-04-17-api-keys-ga.md).

### Can I use Clerk with a Python backend but a mobile frontend?

Yes. Mobile clients (iOS, Android, Expo) acquire session tokens via Clerk's mobile SDKs and send them as `Authorization: Bearer <token>`. The Python backend works identically — `authenticate_request()` doesn't care whether the token came from a browser or a phone.

### How much does Clerk cost for a Python backend?

Pricing applies at the application level, not per SDK. Hobby (free) includes 50,000 MRUs. Pro starts at $20/month billed annually ($25 monthly) and adds MFA, passkeys, custom email templates, and 1 included Enterprise SSO connection. Business is $250/month billed annually ($300 monthly) and adds SOC 2 Report access plus HIPAA artifact access. Enterprise is custom-priced, adds HIPAA compliance with a BAA, and carries a 99.99% uptime SLA. See [clerk.com/pricing](https://clerk.com/pricing) for current numbers.

## In this series

1. [How to Add Authentication to a Python Backend](https://clerk.com/articles/how-to-add-authentication-to-a-python-backend.md)
2. [How to Add Authentication to a Python Backend - Part 2](https://clerk.com/articles/how-to-add-authentication-to-a-python-backend-2.md)
3. **How to Add Authentication to a Python Backend - Part 3** (you are here)
