UseThatApp Logo

    Hosted sign-in

    An optional add-on where UseThatApp runs login for your app ("Sign in with UseThatApp", standard OpenID Connect with PKCE). Buyers come back from checkout already signed in, and get_entitlement tells your server what they bought. Use it only when the app has no accounts of its own; otherwise use license keys.

    What it costs and how to switch it on

    Signing users in is free. Reading what they bought (the entitlement endpoint) needs the Hosted sign-in add-on, a platform-fee uplift with an introductory free period; current terms are at usethatapp.com/pricing. The developer switches it on under Manage app → Integration → Hosted sign-in add-on. It cannot be switched on through MCP. While it is off, entitlement calls answer 403 service_not_enabled.

    Register your URLs

    On Manage app → Integration (or with MCP update_integration_settings):

    SettingWhat it is
    Login URLThe route in your app that starts sign-in. Buyers return here after checkout, and marketplace launches land here. Defaults to the app's URL.
    Redirect URIsYour callback route(s). An exact-match allow-list: the URI your app sends must appear here character for character (scheme, host, port, path, trailing slash).
    Post-logout URLsWhere users may land after signing out. If none is registered, they land on the UseThatApp home page.

    Every URL must be HTTPS on the app's registered domain; localhost and 127.0.0.1 are allowed for development.

    Configure

    .env (server only)
    UTA_CLIENT_ID=<client id>
    UTA_CLIENT_SECRET=<client secret>          # or UTA_CLIENT_SECRET_PATH
    UTA_REDIRECT_URI=https://yourapp.com/callback   # must match a registered redirect URI exactly

    The default scopes, openid entitlements, are what you need. Other settings are in the reference.

    Sign users in

    Two routes. /login starts the flow and keeps a small flow_state in your server-side session. /callback finishes it. Check for ?error= first: it means the user cancelled, which is not a failure. Shown with Flask and Express; the SDK calls are the same in any framework.

    Python — login and callback
    from usethatapp import begin_login, complete_login, UtaError
    
    @app.get("/login")
    def login():
        auth_url, flow_state = begin_login()
        session["uta_flow"] = flow_state
        return redirect(auth_url)
    
    @app.get("/callback")
    def callback():
        flow_state = session.pop("uta_flow", None)
        if request.args.get("error"):
            return redirect("/")                 # user cancelled
        try:
            s = complete_login(code=request.args.get("code"),
                               state=request.args.get("state"),
                               flow_state=flow_state)
        except UtaError:
            return redirect("/login")
        session["uta_sub"] = s.sub
        session["uta_access_token"] = s.access_token
        session["uta_refresh_token"] = s.refresh_token
        session["uta_id_token"] = s.id_token
        return redirect("/")
    JavaScript — login and callback
    import { beginLogin, completeLogin, UtaError } from "usethatapp";
    
    app.get("/login", async (req, res) => {
      const { authorizationUrl, flowState } = await beginLogin();
      req.session.utaFlow = flowState;
      res.redirect(authorizationUrl);
    });
    
    app.get("/callback", async (req, res) => {
      const flowState = req.session.utaFlow;
      delete req.session.utaFlow;
      if (req.query.error) return res.redirect("/");   // user cancelled
      let s;
      try {
        s = await completeLogin({ code: req.query.code, state: req.query.state, flowState });
      } catch (e) {
        if (e instanceof UtaError) return res.redirect("/login");
        throw e;
      }
      Object.assign(req.session, {
        utaSub: s.sub,
        utaAccessToken: s.access_token,
        utaRefreshToken: s.refresh_token,
        utaIdToken: s.id_token,
      });
      res.redirect("/");
    });

    A user already signed in to usethatapp.com passes through without a prompt. A brand-new user who signs up on usethatapp.com must verify their email before a token is issued; buyers who arrive from a buy link are the exception and return signed in straight away.

    Identify users by sub

    sub is the only identity UseThatApp shares: a pairwise pseudonymous id, stable for one user within your app and different in every other app. There is no email and no name. Key your user records on sub; your roles, settings and data hang off that record. userinfo(access_token) returns {"sub": …} and nothing more.

    Gate on the entitlement

    Python — gate
    from usethatapp import get_entitlement
    
    ent = get_entitlement(session["uta_access_token"])
    if ent.entitled and ent.product_public_id == "prod_<pro-id>":
        unlock_pro()
    JavaScript — gate
    import { getEntitlement } from "usethatapp";
    
    const ent = await getEntitlement(req.session.utaAccessToken);
    if (ent.entitled && ent.product_public_id === "prod_<pro-id>") {
      unlockPro();
    }
    • The answer is computed live on every call: a lapsed cancellation or a plan change shows up on the next call. The limit is 60 calls a minute per token, so call it per request or cache it briefly.
    • status is active, trialing, one_time_active, free (your free tier), preview (the developer's own test entitlement), or none. A subscription whose renewal is failing reports as your free tier or none until payment recovers. Gate on entitled, not on status.
    • A visitor with no token at all is your free tier. Let them in, and offer "Sign in with UseThatApp" for paid features.
    • Your app's session and the usethatapp.com session are independent. Signing out of one does not sign out of the other; the only link is the token, so reconcile through get_entitlement.

    Every field: entitlement reference.

    Sign out

    Redirect to logout_url(id_token=…, post_logout_redirect_uri=…) / logoutUrl({ idToken, postLogoutRedirectUri }). Always pass a registered post_logout_redirect_uri: the user may confirm or choose "Stay signed in", and both return to it, so do not clear your session before the redirect. Without one, a confirmed logout lands on the UseThatApp home page and "Stay signed in" on the buyer's dashboard. On return, call get_entitlement: after a confirmed logout the token is revoked and it raises UtaTokenError — clear the session then.

    Python — reconcile after logout
    try:
        ent = get_entitlement(session["uta_access_token"])
    except UtaTokenError:              # 401: logged out, expired, or revoked
        for k in ("uta_sub", "uta_access_token", "uta_refresh_token", "uta_id_token"):
            session.pop(k, None)
        return redirect("/")

    Errors to handle

    ExceptionHTTPDo this
    UtaTokenError401Token expired, revoked, or the user logged out. Try refresh(refresh_token); otherwise clear the session and sign in again.
    UtaServiceNotEnabledError403The add-on is off. Nothing the user does fixes it: alert yourself and serve the free experience. Catch it before UtaPermissionError, which it subclasses.
    UtaPermissionError403The token lacks the entitlements scope. Sign in again with the default scopes.
    UtaAuthError—The callback's state did not match (see stack notes) or the code was missing. Restart sign-in.

    All errors subclass UtaError. The full list is in the reference.

    Stack notes

    • Single-page apps (React, Vue, Svelte). The SDK holds the client secret, so it cannot run in the browser. Put the login, callback and gate calls in a small backend for your frontend, keep the tokens in its session cookie, and have the SPA call it.
    • Streamlit, Dash and other apps without ordinary routes. Run /login and /callback in a small Flask or FastAPI server on the same domain, and have the UI read the signed-in user's plan from it.
    • Next.js, Nuxt. Call the SDK only from server code (route handlers, server actions, Nitro routes), never from a client component.
    • Use one host in development. localhost and 127.0.0.1 have separate cookies. Browse, register the redirect URI, and set UTA_REDIRECT_URI on the same one, or the callback fails with a state mismatch.
    • Show a loading state. Sign-in is a chain of redirects, and a cold server makes it slow. Render a "Signing you in…" page from /callback rather than a blank one.

    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.