ZevAuth
Sign up

Your backend

Python

Verify sessions, manage users and organizations, receive webhooks and serve server-rendered pages from any Python app, sync or async.

pip install zevauth

Python 3.9 or later. Using a framework? Read FastAPI, Django or Flask instead. They build on everything here and wire it into dependencies, middleware and decorators for you.

Who is signed in

import os
from zevauth import Request, ZevAuth

zevauth = ZevAuth(
    os.environ["ZEVAUTH_ENVIRONMENT_ID"],            # env_live_…
    secret_key=os.environ["ZEVAUTH_SECRET_KEY"],      # only for the backend API
)

auth = zevauth.get_auth(Request("GET", url, headers))   # any framework
auth = await zevauth.get_auth_async(request)            # asyncio servers

if auth.is_signed_in:
    auth.user_id              # user_…
    auth.organization_id      # org_…, or None
    auth.has(permission="org:members:manage")

Create one ZevAuth when your app starts and reuse it. It caches your environment’s signing keys, so verifying a request does not touch the network.

The token comes from the Authorization: Bearer … header, or from the session-bridge cookie for server-rendered pages (below). Request takes the method, the full URL, the headers and (only for the session bridge endpoint) the raw body. The framework integrations build it for you.

get_auth() never raises. A missing, expired or tampered token means nobody is signed in, so a page can always decide what to render.

Refusing a request

from zevauth import AuthRefusal

try:
    auth = zevauth.require_auth(request, require_mfa=True)
except AuthRefusal as refused:
    return json_response(refused.body(), status=refused.status)
Statuserror.codeWhen
401unauthorizedNot signed in
403session_task_pendingThe person must finish a task first, such as setting up two-step verification. The body lists pendingTasks
403mfa_requiredYou passed require_mfa=True and they haven’t verified a second factor

Pass allow_pending_tasks=True only on the routes that help someone finish a task. require_auth_async is the same for asyncio.

Verifying a token yourself

from zevauth import ZevAuthVerifyError

try:
    claims = zevauth.verify_token(token)        # or: await zevauth.verify_token_async(token)
except ZevAuthVerifyError as error:
    error.code   # token_expired, invalid_audience, …

Only RS256 is accepted, the audience must be your environment, and clocks may differ by up to five seconds. A token from your test environment is refused on your live one.

For tests, or a server with no outbound network, pass the key set yourself:

zevauth = ZevAuth("env_test_…", jwks=key_set, issuer="https://api.zevauth.net/v1/env_test_…")

Server-rendered pages

A page requested by the browser carries no Authorization header. The session bridge gives your server a cookie to read instead: the browser SDK posts its access token to an endpoint on your site, which sets your own HttpOnly cookie.

zevauth = ZevAuth(
    "env_live_…",
    publishable_key="pk_live_…",       # for the handshake page
    session_bridge=True,
)

Then point the browser SDK at the endpoint:

ZevAuthUI.init({ publishableKey: 'pk_live_…', sessionBridge: { url: '/zevauth/session' } });

Each framework integration mounts the two routes for you (/zevauth/session and /zevauth/handshake). Without one, serve them yourself:

response = zevauth.handle_session_bridge(request)   # POST /zevauth/session
response = zevauth.handshake_page(request)          # GET  /zevauth/handshake
auth, handshake = zevauth.authenticate(request)     # on every page
if handshake is not None:
    return handshake                                 # a redirect, or a 401 for API calls

Every one returns a Response with status, headers (a list, because Set-Cookie repeats) and body.

To change paths, share cookies across subdomains or serve your own copy of the handshake script, pass SessionBridgeOptions:

from zevauth import SessionBridgeOptions

zevauth = ZevAuth(
    "env_live_…",
    publishable_key="pk_live_…",
    session_bridge=SessionBridgeOptions(
        cookie_domain="example.com",
        allowed_origins=["https://app.example.com", "https://www.example.com"],
    ),
)

Backend API

user = zevauth.api.users.create(email="ada@example.com", first_name="Ada")
zevauth.api.users.update(user["id"], public_metadata={"plan": "pro"}, last_name=None)

for user in zevauth.api.users.iterate(metadata={"plan": "pro"}):
    ...

zevauth.api.users.reset_mfa(user["id"], revoke_sessions=True)
zevauth.api.organizations.add_member("org_…", user_id=user["id"], role="org:admin")

Keyword arguments are snake_case; they go out in the API’s camelCase. In update, None clears a field and leaving it out keeps it. zevauth.api_async is the same API for asyncio, with async for over iterate(). You can also build the clients directly:

from zevauth import AsyncZevAuthClient, ZevAuthClient

client = ZevAuthClient(os.environ["ZEVAUTH_SECRET_KEY"])

Errors and retries

from zevauth import ZevAuthError

try:
    zevauth.api.users.get("user_…")
except ZevAuthError as error:
    error.code          # not_found, rate_limited, network_error, …
    error.status        # 404, 429, 0 when no response arrived
    error.request_id    # quote it to ZevAuth support

Rate limits (429) and gateway errors (502, 503, 504) are retried twice, waiting for Retry-After when the API sends it. A 500 is never retried: the request may already have run, and retrying is how one user becomes two.

Webhooks

from zevauth import ZevAuthWebhookError, construct_webhook_event

try:
    event = construct_webhook_event(
        raw_body,                                     # bytes, exactly as received
        headers.get("zevauth-signature"),
        os.environ["ZEVAUTH_WEBHOOK_SECRET"],         # whsec_…
    )
except ZevAuthWebhookError:
    return 400

if event.type == "user.created":
    ...

Verify the raw body. The signature covers the exact bytes ZevAuth sent, so a body that was decoded and re-encoded fails, correctly. Each framework page shows how to get the raw bytes. Deliveries can arrive more than once: use the zevauth-delivery-id header or event.id to handle each one once.

The same in every language

This library implements the same contract as the ZevAuth libraries for Node.js, PHP, Ruby, Go and Java, and passes the same conformance tests. Server SDKs explains what that guarantees.

Source and issues: github.com/zevop/zevauth-python.

Updated at, Sunday, November 1, 2026