UseThatApp Logo
    Python/Flask

    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.

    1. /login — begin_login() returns an authorization URL and a JSON-able flow_state. Stash flow_state in the Flask session and redirect.
    2. /callback — check ?error= first (cancel/deny), then complete_login() exchanges the code for a UtaSession. Persist its sub and tokens.
    3. /logout — redirect to logout_url(...) for RP-initiated sign-out.
    4. 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, 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 flask

    Step 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.

    environment
    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-only

    Step 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.

    app.py
    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.

    app.py
    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.

    app.py
    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:

    app.py
    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.

    app.py
    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:

    app.py
    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.

    app.py
    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.

    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.