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):
| Setting | What it is |
|---|---|
| Login URL | The 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 URIs | Your 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 URLs | Where 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
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 exactlyThe 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.
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("/")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
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()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.
statusisactive,trialing,one_time_active,free(your free tier),preview(the developer's own test entitlement), ornone. A subscription whose renewal is failing reports as your free tier ornoneuntil payment recovers. Gate onentitled, not onstatus.- 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.
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
| Exception | HTTP | Do this |
|---|---|---|
UtaTokenError | 401 | Token expired, revoked, or the user logged out. Try refresh(refresh_token); otherwise clear the session and sign in again. |
UtaServiceNotEnabledError | 403 | The add-on is off. Nothing the user does fixes it: alert yourself and serve the free experience. Catch it before UtaPermissionError, which it subclasses. |
UtaPermissionError | 403 | The 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
/loginand/callbackin 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.
localhostand127.0.0.1have separate cookies. Browse, register the redirect URI, and setUTA_REDIRECT_URIon 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
/callbackrather than a blank one.
