OpenID Connect (Sign in with UseThatApp)
"Sign in with UseThatApp" lets us run login for your app, so a buyer who finishes hosted checkout returns already signed in — and your app reads what they bought from that same session. The same flow serves marketplace launches and everyday logins. Under the hood UseThatApp is an OpenID Provider and this is standard OAuth 2.0 / OpenID Connect; you wire it up once with the usethatapp SDK (Python or JavaScript).
You do not have to use our login
Selling through UseThatApp never requires it. If your app already has accounts, keep them and verify purchases with license keys instead — included in the base per-sale fee. This page is for developers who would rather not run login at all: we handle sign-in, passwords, and recovery, and the license check answers from the same session. That pairing is the Hosted sign-in add-on.
What costs what
Sign-in itself is free and unmetered — wire up OIDC, get identity, pay nothing. The paid part is the entitlement call that turns a session into "and here is what they bought", which the Hosted sign-in add-on enables per app. Build the login first and switch the add-on on when you are ready; without it, entitlement calls answer 403 service_not_enabled.
How the handoff works
It's the standard authorization-code flow with PKCE. The SDK does the crypto; you wire three framework-specific things (read the callback query params, store a small flow_state in your session, issue redirects).
- Arrive. A buyer returns from hosted checkout to your registered Login URL, a user opens your app from the marketplace, or someone clicks "Sign in with UseThatApp" inside it — all three arrive the same way.
- Hand off. Your app sends them to UseThatApp to authenticate (
begin_login). - Sign in. They authenticate on usethatapp.com. If they already have a session, this is seamless — no prompt.
- Return. UseThatApp redirects back to your redirect URI with a one-time code, which your app exchanges for tokens (
complete_login). - Read the plan. Your app calls the entitlement API with the access token to gate features (
get_entitlement).
⚠ The SDK runs on your server, never in the browser.
The usethatapp SDK is a confidential client: it holds your client secret and validates ID tokens. Keep it server-side. A single-page app (React, Vue) should run the SDK in a small backend (a "backend-for-frontend") and call that from the browser — see the React guide.
Set up your app on UseThatApp
Open your app's Integration page on the UseThatApp dashboard. There is no handoff mode to choose — OAuth / OpenID Connect is the only integration for new apps. Copy your client credentials from the API credentials card, then register the URLs below in the Hosted sign-in card underneath it.
| Setting | What it is |
|---|---|
| Client ID / Client secret | Your app's OAuth credentials. The secret is shown once and stored hashed — keep it on your server (UTA_CLIENT_ID, UTA_CLIENT_SECRET). |
| Login URL | The page in your app that starts sign-in. Marketplace launches land here — and it's the default return destination after a completed purchase, so a buyer's first experience of your app is this page signing them in. Defaults to your app URL. |
| Redirect URIs | Where we return the user after sign-in. An allow-list, matched exactly — the URL your app sends must appear here verbatim. Each must be HTTPS on your app's domain (localhost allowed for development). |
| Post-logout return URLs | Where users may land after signing out — also an allow-list. Leave empty and they return to the UseThatApp home page instead of your app. |
Install and configure
Install the SDK and set these in your server's environment. Only the client id and secret are required; the rest default to production.
UTA_CLIENT_ID=... # required
UTA_CLIENT_SECRET=... # required (server-side only)
UTA_REDIRECT_URI=https://yourapp.com/auth/callback
# optional — default to production:
UTA_ISSUER=https://www.usethatapp.com/o
UTA_API_URL=https://www.usethatapp.com
UTA_SCOPES="openid entitlements"Sign a user in
begin_login() returns an authorization URL and a JSON-serializable flow_state — stash it in the session, redirect to the URL, then pass it back to complete_login()in your callback. On cancel/deny, the provider returns with ?error= and no code, so check for that first.
from usethatapp import begin_login, complete_login
# 1) Start login:
auth_url, flow_state = begin_login()
save_to_session("uta_flow", flow_state)
return redirect(auth_url)
# 2) In your callback (reads ?code=&state=, or ?error= on cancel):
if read_query("error"):
return redirect("/") # login canceled
session = complete_login(
code=read_query("code"),
state=read_query("state"),
flow_state=load_from_session("uta_flow"),
)
save_to_session("uta_sub", session.sub)
save_to_session("uta_access_token", session.access_token)import { beginLogin, completeLogin } from "usethatapp";
// 1) Start login:
const { authorizationUrl, flowState } = await beginLogin();
saveToSession("utaFlow", flowState);
res.redirect(authorizationUrl);
// 2) In your callback:
if (req.query.error) return res.redirect("/"); // login canceled
const session = await completeLogin({
code: req.query.code,
state: req.query.state,
flowState: loadFromSession("utaFlow"),
});Identity: the pairwise sub
The login result carries the user's sub — a pairwise pseudonymous identifier. It is stable for a user within your app, but different in every other app, so it can't be correlated across apps. It's the only identity claim we share: no email, no name, no PII.
- Use
subas your local user key — it's persistent across logins for your app. - Key your user records off
sub, never an email (we don't share one, and emails change). - Identity is delivered at login (
session.sub); it is not in the entitlement response, which only describes the plan.
Need to re-check who a stored access token belongs to — say, after restoring a session from your database? userinfo() asks the server and returns the token's claims. Like everything else here, that's the sub and nothing more:
from usethatapp import userinfo
claims = userinfo(access_token)
claims["sub"] # the same pairwise id complete_login() delivered
# No email, no name — {"sub": "..."} is the entire response.Read the plan: the entitlement API
Call get_entitlement(access_token) whenever you need the user's current plan. It's always authoritative — a canceled license stops being entitled immediately, regardless of token lifetime. For the full licensing story — every status, selling from your own website, and the raw HTTP endpoint — see Licensing.
from usethatapp import get_entitlement
ent = get_entitlement(access_token)
# Entitlement(entitled, version, product_public_id, status, is_free, period_end)
if ent.entitled and ent.product_public_id == "prod_<your-pro-id>":
unlock_pro_features()| Field | Meaning |
|---|---|
entitled | True if the user may use the app (an active license, or a free tier). |
product_public_id | Stable, opaque product identifier (prod_…). Gate your features on this — it survives a plan rename, and it matches the product_id on every Pricing API price. |
product_id | Equal-valued alias of product_public_id — both carry the same prod_… value. Gate on either. |
version | Plan display name (e.g. "Pro"). For showing the user; mutable, so don't branch logic on it. |
status | active / trialing / one_time_active / free / preview / none |
is_free / period_end | Whether it's the free tier; the ISO date the current period ends (or null). |
Sign out
Sign-out is RP-initiated: redirect the user to logout_url(id_token=…). Logging out of UseThatApp revokes the app's tokens, so the app loses access — this is the intended "log out everywhere" behavior.
⚠ Reconcile on return — don't clear your session eagerly.
Both outcomes — the user confirms, or chooses "Stay signed in" — return to your post-logout URL, so you can't tell which happened from the redirect. Check the token instead: after returning, a confirmed logout makes get_entitlement raise UtaTokenError (401) — drop the token then. If they stayed signed in, the token is still valid and they keep their session. Clearing your session before logout logs the user out even when they chose to stay.
Errors
get_entitlement distinguishes two auth failures so you can tell "re-authenticate" from "not permitted":
- 401 →
UtaTokenError— the access token is missing, expired, or revoked. Refresh, or send the user back through login. - 403 →
UtaPermissionError— a valid token that lacks theentitlementsscope.
Every SDK error inherits from UtaError. See Error Handling.
Good to know
- Seamless SSO. A signed-in user launching your app skips the consent screen and goes straight in — safe because only an opaque pairwise
subis shared. - Two independent sessions. UseThatApp's session and your app's session are separate by design. "Logged into your app" and "logged into usethatapp.com" are different states.
- Free vs paid launches. A free launch carries no login by design — an anonymous visitor with no token is your free tier. A paid launch signs the user in.
- First-ever signup isn't instant. A brand-new user signing up on usethatapp.com must verify their email before a token is issued; subsequent logins are seamless. SSO doubles as a signup funnel for you. The exception is buyers arriving through a purchase link: their account is created during checkout and they return with a token immediately, verifying their email afterward from the receipt email.
Next steps
Pick your stack — the framework guides give concrete, copy-ready code for Python and JavaScript. Then review Error Handling and Gotchas.
