UseThatApp Logo
    Python/FastAPI

    FastAPI

    Sign users in with their UseThatApp account and read their live plan in a FastAPI app, over OAuth 2.0 / OpenID Connect. These routes wire your app into the whole selling loop: a buyer who purchases from your own website lands back here signed in, entitlement already live — and marketplace launches arrive the same way. The code on this page is taken directly from the tested examples/fastapi_min/ in the Python SDK. For the conceptual model, see OpenID Connect.

    This guide covers the Hosted sign-in path

    It has UseThatApp run login, so buyers return signed in and the same session answers what they bought — the Hosted sign-in add-on. If your app already has its own accounts, you do not need any of the sign-in wiring below: keep your login and verify purchases with a license key your server validates in one call, included in the per-sale fee.

    How it works

    Three routes plus a gated home view. The usethatapp SDK runs on your server as a confidential client. FastAPI is async, so use get_entitlement_async on the hot path; the one-time callback can call the sync complete_login (it's just a login round-trip).

    1. /login — begin_login() then a RedirectResponse. Stash flow_state in the session.
    2. /callback — check the error param first, then complete_login().
    3. /logout — redirect to logout_url(...).
    4. Gated view — await get_entitlement_async(access_token).

    Prerequisites

    • Python 3.9+ and a FastAPI app served by Uvicorn.
    • Starlette's SessionMiddleware (it requires itsdangerous) — flow_state and the tokens live in the signed-cookie session.
    • UTA_CLIENT_ID, UTA_CLIENT_SECRET, and UTA_REDIRECT_URI in your server environment. Register the OAuth client and redirect URI on your app's Integration page on the UseThatApp dashboard.

    Step 1 — Install

    terminal
    pip install usethatapp fastapi uvicorn itsdangerous

    Step 2 — Configure

    environment
    export UTA_CLIENT_ID=...
    export UTA_CLIENT_SECRET=...                 # server-side only
    export UTA_REDIRECT_URI=http://localhost:8000/callback
    # Pointing at a non-production instance? Set BOTH of these
    # (each defaults to production):
    # export UTA_ISSUER=http://localhost:8000/o
    # export UTA_API_URL=http://localhost:8000

    Step 3 — App and session middleware

    Add SessionMiddleware so request.session is available to the login flow.

    app.py
    import os
    
    from fastapi import FastAPI, Request
    from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
    from starlette.middleware.sessions import SessionMiddleware
    
    from usethatapp import (
        UtaError,
        UtaTokenError,
        begin_login,
        complete_login,
        get_entitlement_async,
        logout_url,
    )
    
    app = FastAPI()
    app.add_middleware(SessionMiddleware, secret_key=os.environ.get("SESSION_SECRET", "dev-only"))

    Step 4 — Login and callback

    begin_login() returns the authorization URL and a flow_state dict; stash it in the session and redirect. In the callback, check the error param first (cancel/deny), then exchange the code with complete_login() and persist s.sub as your local user key.

    app.py
    @app.get("/login")
    def login(request: Request):
        auth_url, flow_state = begin_login()
        request.session["uta_flow"] = flow_state
        return RedirectResponse(auth_url)
    
    
    @app.get("/callback")
    def callback(request: Request, code: str = "", state: str = "", error: str = ""):
        # On cancel/deny, OAuth redirects back with ?error=... and no code.
        if error:
            request.session.pop("uta_flow", None)
            return RedirectResponse("/")
        try:
            s = complete_login(
                code=code, state=state, flow_state=request.session.pop("uta_flow", {})
            )
        except UtaError as e:
            return JSONResponse({"error": str(e)}, status_code=400)
        request.session["uta_sub"] = s.sub
        request.session["uta_access_token"] = s.access_token
        request.session["uta_id_token"] = s.id_token
        return RedirectResponse("/")

    Step 5 — Gate on the live plan (async)

    On the request hot path use await get_entitlement_async(token). Gate on the stable ent.product_public_id, not the mutable ent.version. A UtaTokenError (401) means the token was revoked or expired — reconcile by dropping it.

    app.py
    @app.get("/")
    async def home(request: Request):
        token = request.session.get("uta_access_token")
        if token:
            try:
                ent = await get_entitlement_async(token)
                return {"sub": request.session.get("uta_sub"), "entitlement": ent.raw}
            except UtaTokenError:
                # Token revoked/expired (signed out of UseThatApp). Reconcile.
                for k in ("uta_access_token", "uta_sub", "uta_id_token"):
                    request.session.pop(k, None)
        return HTMLResponse('<a href="/login">Log in with UseThatApp</a>')

    Step 6 — Sign out

    Redirect to logout_url(...) for RP-initiated sign-out. Don't clear your session eagerly: both "Stay signed in" and a confirmed logout return to the same post-logout URL. A confirmed logout revokes the token, so the next get_entitlement_async in home raises UtaTokenError and drops it.

    app.py
    @app.get("/logout")
    def logout(request: Request):
        id_token = request.session.get("uta_id_token")
        return RedirectResponse(
            logout_url(id_token=id_token, post_logout_redirect_uri="http://localhost:8000/")
        )

    Offer the upgrade: purchase links

    When the entitlement check says the user is not entitled — or is on the free tier — send them to hosted checkout with purchase_url(). UseThatApp is the merchant of record (payment, tax, refunds), and the buyer comes back to your app signed in with the entitlement already live: no webhooks, no polling. purchase_url() and manage_url() are pure URL builders — no network call, so there is nothing to await.

    app.py
    from usethatapp import manage_url, purchase_url
    
    PRO_PRICE_ID = "<prc-price-id>"   # from get_prices_async() or your dashboard
    
    
    @app.get("/account")
    async def account(request: Request):
        token = request.session.get("uta_access_token")
        if not token:
            return RedirectResponse("/login")
        ent = await get_entitlement_async(token)
        if ent.entitled and ent.product_public_id == "prod_<your-pro-id>":
            # Already entitled — offer subscription management instead.
            manage = manage_url(next="http://localhost:8000/account")
            return HTMLResponse(f'<a href="{manage}">Manage subscription</a>')
        # Not entitled — hosted checkout; the buyer returns signed in, entitled.
        buy = purchase_url(PRO_PRICE_ID, next="http://localhost:8000/welcome")
        return HTMLResponse(f'<a href="{buy}">Upgrade to Pro</a>')

    For a live pricing page, render from await get_prices_async() — never hardcode prices. Each Price carries a ready-made buy_url and the product_id that matches the entitlement’s product_public_id. See the Pricing API and Sell from your own website.

    Run it

    terminal
    uvicorn app:app

    Errors

    At the entitlement endpoint, 401 raises UtaTokenError (re-authenticate or refresh) and 403 raises UtaPermissionError (a valid token missing the entitlements scope). Every SDK error inherits from UtaError. See Error Handling and Gotchas.

    AI agents: this site is fully pre-rendered — fetch /llms.txt for an indexed summary of every page, or /llms-full.txt for the whole documentation as one Markdown file.