Verify access tokens and handle webhooks in any Python framework.

Python SDK

The ascendkit Python package provides access token verification and webhook signature validation. It works with any Python framework — FastAPI, Flask, Django, Starlette, or plain scripts.

What this SDK does and does not do

The Python SDK covers verification, user management, and OAuth initiation. It does not render sign-in UI, issue sessions, or send magic links — those live in the JS SDK, whose server-side auth runtime requires Next.js.

CapabilityPython SDKJS SDK (@ascendkit/nextjs)
Verify access tokensYes — AccessTokenVerifierYes
Verify webhook signaturesYes — verify_webhook_signatureYes
Server-side analyticsYes — AnalyticsYes
Read and update usersYes — AuthClientYes
Start an OAuth redirect flowYes — build_authorize_urlYes
Render sign-in UINoYes — drop-in components
Credentials and magic link sign-inNoYes
Issue and manage sessionsNoYes

So a Python-only application can authenticate users through OAuth: build the authorize URL, let AscendKit handle the provider round-trip, and verify the resulting token. What you do not get is a rendered sign-in form or password and magic-link flows — for those you need the JS SDK.

How the pieces fit together

Browser  ──►  Next.js app          ──►  AscendKit
              (@ascendkit/nextjs)        issues access token
                    │
                    │  Authorization: Bearer <token>
                    ▼
              Python backend
              (ascendkit)  ──► verifier.verify(token) ──► claims

Verification is local and offline after the first call. The SDK fetches AscendKit's JWKS once, caches the keys in memory for an hour, and validates the RS256 signature itself. There is no network round-trip to AscendKit per request, so token checks stay fast and survive brief AscendKit outages.

Install

pip install ascendkit

Requires Python 3.10+. Dependencies: httpx, pydantic, pyjwt[crypto].

Access token verification

Verify RS256 access tokens issued by AscendKit using JWKS. Keys are cached in memory for 1 hour.

from ascendkit import AccessTokenVerifier, AuthError

verifier = AccessTokenVerifier()

# Sync
claims = verifier.verify(token)
print(claims["sub"])  # usr_...
print(claims["email"])

# Async
claims = await verifier.verify_async(token)

The constructor reads ASCENDKIT_ENV_KEY from your environment. If the variable is missing, it raises an error at initialization so you catch configuration issues immediately.

Raises AuthError with appropriate status codes:

  • 401 — expired or invalid token
  • 503 — JWKS endpoint unreachable

The distinction matters when you decide how to respond. A 401 is the client's problem and they should re-authenticate. A 503 is yours — AscendKit's JWKS endpoint was unreachable and the key cache had expired, so returning 401 would incorrectly sign out every valid user. Map them to different responses rather than collapsing both into "unauthorized".

Which claims are available

verify() returns the decoded claims dictionary. The two you will use constantly:

ClaimContainsExample
subThe prefixed user IDusr_abc123
emailThe user's verified email[email protected]

Use sub as the foreign key in your own tables. It is stable, it is the same identifier that email, journeys, and surveys address, and it is safe to expose in APIs — unlike an internal database ID.

Framework integration

from fastapi import FastAPI, Depends, Header, HTTPException
from ascendkit import AccessTokenVerifier, AuthError

app = FastAPI()
verifier = AccessTokenVerifier()

async def get_current_user(authorization: str = Header()) -> dict:
    token = authorization.removeprefix("Bearer ").strip()
    try:
        return await verifier.verify_async(token)
    except AuthError as e:
        raise HTTPException(status_code=e.status_code, detail=str(e))

@app.get("/api/profile")
async def profile(user: dict = Depends(get_current_user)):
    return {"email": user["email"], "id": user["sub"]}

Webhook verification

Verify webhook signatures using HMAC-SHA256. Includes timestamp validation (5-minute tolerance) to prevent replay attacks.

from fastapi import Request, Response
from ascendkit import verify_webhook_signature
import os

WEBHOOK_SECRET = os.environ["ASCENDKIT_WEBHOOK_SECRET"]

@app.post("/webhooks/ascendkit")
async def handle_webhook(request: Request) -> Response:
    body = await request.body()
    signature = request.headers.get("x-ascendkit-signature", "")

    if not verify_webhook_signature(
        secret=WEBHOOK_SECRET,
        signature_header=signature,
        payload=body.decode(),
    ):
        return Response(status_code=401, content="Invalid signature")

    event = await request.json()
    # Handle event by type: user.created, user.approved, etc.
    return Response(status_code=200, content="OK")

Server-side analytics

Track events from your backend with trusted identity (secret key auth):

from ascendkit import Analytics

analytics = Analytics()

analytics.track("usr_456", "checkout.completed", {"total": 99.99})

The constructor reads ASCENDKIT_SECRET_KEY from your environment. If missing, it raises an error at initialization.

Events batch in memory and flush every 30 seconds or when the batch fills (10 events). Call analytics.shutdown() for graceful cleanup.

Troubleshooting

AccessTokenVerifier() raises at import time

Deliberate. The constructor reads ASCENDKIT_ENV_KEY from the environment and raises immediately if it is missing, so a misconfigured deployment fails at startup rather than on the first authenticated request.

Set ASCENDKIT_ENV_KEY before the module is imported. In containerized deployments this usually means the environment variable is missing from the runtime config even though it exists in your build config.

Tokens verify locally but fail with 401 in production

Almost always a key mismatch: the token was issued against one environment and is being verified against another. Development and production have different keys.

Confirm the ASCENDKIT_ENV_KEY on the backend belongs to the same environment your frontend authenticates against.

Intermittent 503s under load

The JWKS cache expired and several requests tried to refresh it at once while the endpoint was slow.

Keys cache for one hour, so this is rare. If it recurs, treat 503 as retryable rather than as an auth failure — do not sign the user out.

Webhook signature verification always fails

Nearly always the payload has been re-serialized before verification. The signature is computed over the exact bytes AscendKit sent, so parsing JSON and re-encoding it produces a different string.

Pass the raw request body — await request.body() in FastAPI, request.get_data(as_text=True) in Flask — never request.json() re-dumped. If the raw body is correct and it still fails, check that server clocks are accurate: verification enforces a five-minute timestamp tolerance to prevent replay.

Analytics events never appear

Events batch in memory and flush every 30 seconds or at 10 events. A short-lived script that exits before a flush loses them.

Call analytics.shutdown() before exit. Note also that track() requires a usr_ ID — there is no anonymous event path, so analytics cannot be used to record unauthenticated traffic.

Related

  • Integration — the Next.js side that issues the tokens this SDK verifies
  • Webhooks — configuring the endpoints and events you verify here
  • Analytics — what the event data looks like once it lands
  • Installation — environment variables and their server/client split