Django
Sign users in with their UseThatApp account and read their live plan in a Django app, over OAuth 2.0 / OpenID Connect. These views 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/django_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 views plus a gated home view, wired into your URL conf. The usethatapp SDK runs on your server as a confidential client and reads its config from django.conf.settings when Django is installed (falling back to environment variables otherwise).
login—begin_login()returns an authorization URL and a JSON-ableflow_state; stash it inrequest.sessionand redirect.callback— checkrequest.GET["error"]first, thencomplete_login()returns aUtaSession.logout— redirect tologout_url(...)for RP-initiated sign-out.- Gated view — call
get_entitlement(access_token)for the live plan.
Prerequisites
- Python 3.9+ and a Django project.
- The session middleware enabled (
django.contrib.sessions.middleware.SessionMiddleware) —flow_stateand the tokens live in the session. UTA_CLIENT_ID,UTA_CLIENT_SECRET, andUTA_REDIRECT_URIavailable — set them in your environment or add them tosettings.py. Register the OAuth client and redirect URI on your app's Integration page on the UseThatApp dashboard.
Step 1 — Install
pip install usethatapp djangoStep 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 — Views
Read the callback query params from request.GET, store flow_state in request.session, and issue redirects. Persist session.sub as your local user key.
from django.http import HttpResponse, JsonResponse
from django.shortcuts import redirect
from usethatapp import (
UtaError,
UtaTokenError,
begin_login,
complete_login,
get_entitlement,
logout_url,
)
def login(request):
auth_url, flow_state = begin_login()
request.session["uta_flow"] = flow_state # stash for the callback
return redirect(auth_url)
def callback(request):
# On cancel/deny, OAuth redirects back with ?error=... and no code.
if request.GET.get("error"):
request.session.pop("uta_flow", None)
return redirect("home")
try:
session = complete_login(
code=request.GET.get("code"),
state=request.GET.get("state"),
flow_state=request.session.pop("uta_flow", {}),
)
except UtaError as e:
return HttpResponse(f"login failed: {e}", status=400)
# Persist what you need against your own session.
request.session["uta_sub"] = session.sub
request.session["uta_access_token"] = session.access_token
request.session["uta_id_token"] = session.id_token
return redirect("home")
def home(request):
token = request.session.get("uta_access_token")
if token:
try:
ent = get_entitlement(token)
return JsonResponse({"sub": request.session.get("uta_sub"), "entitlement": ent.raw})
except UtaTokenError:
# Token revoked/expired (e.g. signed out of UseThatApp). Reconcile
# by dropping it; fall through to the logged-out view.
for k in ("uta_access_token", "uta_sub", "uta_id_token"):
request.session.pop(k, None)
return HttpResponse('<a href="/login/">Log in with UseThatApp</a>')
def logout(request):
# Don't clear the session yet — the user may choose "Stay signed in" at
# UseThatApp. Reconcile on return: a real logout revokes the token, so the
# next get_entitlement (in home) returns 401 and we drop it then.
id_token = request.session.get("uta_id_token")
return redirect(logout_url(id_token=id_token, post_logout_redirect_uri="http://localhost:8000/"))Step 4 — URLs
Wire the views into your URL conf. The callback path must match the path component of your registered UTA_REDIRECT_URI.
from django.urls import path
from . import views
urlpatterns = [
path("", views.home, name="home"),
path("login/", views.login, name="login"),
path("callback/", views.callback, name="callback"),
path("logout/", views.logout, name="logout"),
]Gate on the live plan
Gate features on the stable ent.product_public_id, not the mutable ent.version display name. The entitlement is always authoritative — a canceled license stops being entitled immediately.
def dashboard(request):
token = request.session.get("uta_access_token")
if not token:
return redirect("login")
ent = get_entitlement(token)
if ent.entitled and ent.product_public_id == "prod_<your-pro-id>":
return render(request, "dashboard_pro.html")
return render(request, "dashboard_free.html")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
def account(request):
token = request.session.get("uta_access_token")
if not token:
return redirect("login")
ent = get_entitlement(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 render(request, "account.html", {"manage": manage})
# Not entitled — hosted checkout; the buyer returns signed in, entitled.
buy = purchase_url(PRO_PRICE_ID, next="http://localhost:8000/welcome/")
return render(request, "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.
