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).
/login—begin_login()then aRedirectResponse. Stashflow_statein the session./callback— check theerrorparam first, thencomplete_login()./logout— redirect tologout_url(...).- Gated view —
await get_entitlement_async(access_token).
Prerequisites
- Python 3.9+ and a FastAPI app served by Uvicorn.
- Starlette's
SessionMiddleware(it requiresitsdangerous) —flow_stateand the tokens live in the signed-cookie session. UTA_CLIENT_ID,UTA_CLIENT_SECRET, andUTA_REDIRECT_URIin your server environment. Register the OAuth client and redirect URI on your app's Integration page on the UseThatApp dashboard.
Step 1 — Install
pip install usethatapp fastapi uvicorn itsdangerousStep 2 — Configure
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:8000Step 3 — App and session middleware
Add SessionMiddleware so request.session is available to the login flow.
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.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.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.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.
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
uvicorn app:appErrors
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.
