Flask
Sign users in with their UseThatApp account and read their live plan in a Flask 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/flask_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 do all the work. The usethatapp SDK runs on your server as a confidential client — it holds the client secret and validates ID tokens.
/login—begin_login()returns an authorization URL and a JSON-ableflow_state. Stashflow_statein the Flask session and redirect./callback— check?error=first (cancel/deny), thencomplete_login()exchanges the code for aUtaSession. Persist itssuband tokens./logout— redirect tologout_url(...)for RP-initiated sign-out.- Gated view — call
get_entitlement(access_token)to read the live plan.
Prerequisites
- Python 3.9+ and a Flask app.
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 flaskStep 2 — Configure
The SDK reads UTA_* from the environment. Only the client id, secret, and redirect URI are required; the rest default to production. The redirect URI here must match a redirect URI registered on the dashboard, verbatim.
export UTA_CLIENT_ID=...
export UTA_CLIENT_SECRET=... # server-side only
export UTA_REDIRECT_URI=http://localhost:5000/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
export FLASK_SECRET_KEY=dev-onlyStep 3 — Start login
begin_login() returns the authorization URL and a small flow_state dict. Save it in the session so the callback can validate the round-trip, then redirect.
from flask import Flask, redirect, request, session, url_for
from usethatapp import begin_login
@app.route("/login")
def login():
auth_url, flow_state = begin_login()
session["uta_flow"] = flow_state
return redirect(auth_url)Step 4 — Handle the callback
Check the error query param first: on cancel/deny the provider redirects back with ?error= and no code. Otherwise complete_login() validates state, exchanges the code, and verifies the ID token, returning a UtaSession. Persist its sub (your local user key), access token, and id token.
from usethatapp import UtaError, complete_login
@app.route("/callback")
def callback():
# On cancel/deny, OAuth redirects back with ?error=... and no code.
if request.args.get("error"):
session.pop("uta_flow", None)
return redirect(url_for("home"))
try:
s = complete_login(
code=request.args.get("code"),
state=request.args.get("state"),
flow_state=session.pop("uta_flow", {}),
)
except UtaError as e:
return f"login failed: {e}", 400
session["uta_sub"] = s.sub
session["uta_access_token"] = s.access_token
session["uta_id_token"] = s.id_token
return redirect(url_for("home"))Step 5 — Gate on the live plan
Call get_entitlement(access_token) wherever you need the user's current plan. It's always authoritative. A 401 means the token is expired or revoked (UtaTokenError) — reconcile by dropping it and falling back to the logged-out view.
from usethatapp import UtaTokenError, get_entitlement
@app.route("/")
def home():
token = session.get("uta_access_token")
if token:
try:
ent = get_entitlement(token)
return {"sub": 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"):
session.pop(k, None)
return '<a href="/login">Log in with UseThatApp</a>'Gate features on the stable ent.product_public_id, not the mutable ent.version display name:
ent = get_entitlement(token)
if ent.entitled and ent.product_public_id == "prod_<your-pro-id>":
return render_template("dashboard_pro.html")
return render_template("dashboard_free.html")Step 6 — Sign out
Sign-out is RP-initiated: redirect to logout_url(...). Don't clear your session eagerly — the user may choose "Stay signed in", and both outcomes return to the same post-logout URL. Reconcile on return instead: a confirmed logout revokes the token, so the next get_entitlement in home raises UtaTokenError and drops it there.
from usethatapp import logout_url
@app.route("/logout")
def logout():
id_token = session.get("uta_id_token")
return redirect(
logout_url(id_token=id_token, post_logout_redirect_uri="http://localhost:5000/")
)Complete example
The full examples/flask_min/app.py from the SDK:
import os
from flask import Flask, redirect, request, session, url_for
from usethatapp import (
UtaError,
UtaTokenError,
begin_login,
complete_login,
get_entitlement,
logout_url,
)
app = Flask(__name__)
app.secret_key = os.environ.get("FLASK_SECRET_KEY", "dev-only")
@app.route("/login")
def login():
auth_url, flow_state = begin_login()
session["uta_flow"] = flow_state
return redirect(auth_url)
@app.route("/callback")
def callback():
# On cancel/deny, OAuth redirects back with ?error=... and no code.
if request.args.get("error"):
session.pop("uta_flow", None)
return redirect(url_for("home"))
try:
s = complete_login(
code=request.args.get("code"),
state=request.args.get("state"),
flow_state=session.pop("uta_flow", {}),
)
except UtaError as e:
return f"login failed: {e}", 400
session["uta_sub"] = s.sub
session["uta_access_token"] = s.access_token
session["uta_id_token"] = s.id_token
return redirect(url_for("home"))
@app.route("/")
def home():
token = session.get("uta_access_token")
if token:
try:
ent = get_entitlement(token)
return {"sub": 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"):
session.pop(k, None)
return '<a href="/login">Log in with UseThatApp</a>'
@app.route("/logout")
def logout():
# Don't clear the session yet — the user may choose "Stay signed in". A
# real logout revokes the token, so the next get_entitlement (home) 401s
# and we drop it then.
id_token = session.get("uta_id_token")
return redirect(logout_url(id_token=id_token, post_logout_redirect_uri="http://localhost:5000/"))
if __name__ == "__main__":
app.run(port=5000)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. Both purchase_url() and manage_url() are pure URL builders — no network call.
from usethatapp import get_entitlement, manage_url, purchase_url
PRO_PRICE_ID = "<prc-price-id>" # from get_prices() or your dashboard
@app.route("/account")
def account():
ent = get_entitlement(session["uta_access_token"])
if ent.entitled and ent.product_public_id == "prod_<your-pro-id>":
# Already on Pro — offer subscription management instead.
manage = manage_url(next="http://localhost:5000/account")
return render_template("account.html", manage=manage)
# Not on Pro — hosted checkout; the buyer returns signed in, entitled.
buy = purchase_url(PRO_PRICE_ID, next="http://localhost:5000/welcome")
return render_template("upgrade.html", buy=buy)For a live pricing page, render from get_prices() — 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.
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.
