Error Handling
Every failure mode in both SDKs — sign-in, entitlement checks, and the purchase/pricing surface — maps to a specific exception class that inherits from UtaError. This page lists each error, what triggers it, and the user-facing behavior we recommend.
The Error Hierarchy
All SDK exceptions inherit from a single base, so you can catch everything in one place if you want:
UtaError (base — catch this for "any SDK failure")
├── UtaConfigError local: missing or invalid config (client id/secret, redirect URI)
├── UtaDiscoveryError couldn't fetch the OpenID discovery doc / JWKS
├── UtaAuthError login flow: state mismatch, user canceled, bad authorize request
├── UtaTokenError 401: code exchange failed, ID-token invalid, or access token expired/revoked
├── UtaPermissionError 403: valid token missing the "entitlements" scope
│ └── UtaServiceNotEnabledError 403: the app hasn't enabled the Hosted sign-in add-on
│ (a SUBCLASS — catch it BEFORE UtaPermissionError)
├── UtaNotFoundError 404: unknown license key, order ref, or license id (.code says which)
├── UtaOrderProcessingError 202: order is genuine but its license hasn't landed yet — retry
├── UtaLicenseCanceledError 409: can't regenerate a key whose license is terminally gone
└── UtaServerError 5xx from the provider (retriable with backoff)Class names are identical in Python and JavaScript. Import them from usethatapp in either language.
Which errors you can actually hit
The first six are Hosted sign-in errors — they come from the login flow and the entitlement call. The three after them belong to the License Key API and only appear if you verify with keys. UtaConfigError and UtaServerError apply to both. Note what is not here: a canceled or refunded purchase never raises — it returns normally with entitled false, in either mode.
Where Errors Come From
The SDKs surface errors at two points:
- Login —
complete_login(...)/completeLogin(...)can raiseUtaAuthError(thestatedidn't match, or the user canceled) orUtaTokenError(the authorization code couldn't be exchanged, or the returned ID token failed validation). Discovery problems surface asUtaDiscoveryError. - Entitlement —
get_entitlement(access_token)/getEntitlement(accessToken)can raiseUtaTokenError(401),UtaPermissionError(403),UtaServerError(5xx), or a plainUtaError(400 — the client isn't linked to an app). - Pricing / app info —
get_prices()/getPrices()andget_app_info()/getAppInfo()hit the anonymous public API and can raise a plainUtaError(404 — the app is unknown, unpublished, or external sales isn't enabled for it yet) orUtaServerError(429 rate limit — 120 requests/minute per IP — or 5xx/network; both retriable). - URL builders —
purchase_url/purchaseUrlandmanage_url/manageUrlmake no network calls. They only fail locally: an empty price id (PythonValueError, JavaScriptUtaError), or a missingUTA_CLIENT_IDfor the manage URL (UtaConfigError).
Errors at a Glance
| Exception | Trigger | Source | Recommended handling |
|---|---|---|---|
UtaConfigError | Missing or invalid config (no UTA_CLIENT_ID/UTA_CLIENT_SECRET/UTA_REDIRECT_URI) | Local, at first SDK call | Log + fail the request with HTTP 500. This is a deploy-time bug; fix the env config and redeploy. |
UtaDiscoveryError | Couldn't fetch the OpenID discovery document or JWKS from the issuer | Login / entitlement | Usually a network blip or a misconfigured UTA_ISSUER. Retry; if it persists, check outbound connectivity to usethatapp.com. |
UtaAuthError | The state didn't match (CSRF/cookie issue), or the user canceled / denied consent | complete_login | Treat as "login canceled" — send the user back to start the flow again. Check the callback error param first so a clean cancel never reaches this. |
UtaTokenError | HTTP 401 — the access token is missing, expired, or revoked; or, at login, the code exchange / ID-token validation failed | get_entitlement / complete_login | Re-authenticate or refresh. On the entitlement path this is the normal signal that the user signed out of UseThatApp — reconcile and drop the token. See the section below. |
UtaPermissionError | HTTP 403 — a valid token that lacks the entitlements scope | get_entitlement | Re-run login requesting openid entitlements (the default UTA_SCOPES). If you narrowed the scopes, widen them. |
UtaError (base, 400) | HTTP 400 — your OAuth client isn't linked to an app (misconfiguration) | get_entitlement | Log + alert. Confirm the client on the dashboard's Integration page is attached to your app. |
UtaError (base, 404) | HTTP 404 from the public API — the app is unknown, unpublished, or external sales isn't enabled for it yet | get_prices / get_app_info | Check the UTA_CLIENT_ID, then the readiness list on the app's manage page: seller verified, app published, product active, external sales on. |
UtaServerError | HTTP 5xx (transient), or 429 (rate limited — public API: 120/min per IP; entitlement: 60/min per token; License Key API: 120/min validate and order lookup, 30/min regenerate, per caller IP + client id). 429s carry a Retry-After header, surfaced on the exception as retry_after (Python) / retryAfter (JavaScript) in seconds — use it as your backoff interval. | get_entitlement / get_prices / get_app_info / validate_license_key / get_order / regenerate_license_key | Retry with exponential backoff. For entitlements, optionally serve the last-known plan while retrying; for prices, serve the last-fetched list (responses are cacheable for 60s anyway). |
The two auth failures: 401 vs 403
get_entitlement distinguishes "re-authenticate" from "not permitted" so you can respond correctly:
- 401 →
UtaTokenError— the access token is missing, expired, or revoked.refresh(refresh_token), or send the user back through login. - 403 →
UtaPermissionError— the token is valid but lacks theentitlementsscope. Re-run login with the right scopes.
UtaTokenError is also your sign-out signal
⚠ Reconcile on return — don't clear the session eagerly at logout.
Sign-out is RP-initiated. A confirmed logout revokes the token, so the next get_entitlement raises UtaTokenError (401). That's your cue to drop the token. If the user chose "Stay signed in," the token is still valid and they keep their session. See Gotchas for the full pattern.
Python pattern
from usethatapp import (
UtaError,
UtaTokenError,
UtaPermissionError,
UtaServiceNotEnabledError,
get_entitlement,
)
def gated_view(request):
token = request.session.get("uta_access_token")
if not token:
return redirect("/login")
try:
ent = get_entitlement(token)
except UtaTokenError:
# 401: token expired/revoked (or the user signed out). Reconcile and
# re-authenticate — try refresh() first if you stored a refresh token.
for k in ("uta_access_token", "uta_sub", "uta_id_token"):
request.session.pop(k, None)
return redirect("/login")
except UtaServiceNotEnabledError:
# 403: Hosted sign-in is switched off for this app. Nothing the
# USER does can fix it — no retry, refresh, or re-consent helps.
# Alert yourself (the developer) and degrade gracefully. Catch
# this BEFORE UtaPermissionError: it is a subclass, so the scope
# branch below would swallow it into a re-consent loop.
logger.error("Hosted sign-in add-on is disabled for this app")
return render(request, "dashboard.html", {"version": None})
except UtaPermissionError:
# 403: valid token, missing the "entitlements" scope.
return redirect("/login") # re-consent with openid entitlements
except UtaError as e:
# Discovery / server / config failure: log + degrade to free.
logger.exception("get_entitlement failed: %s", e)
return render(request, "dashboard.html", {"version": None})
return render(request, "dashboard.html", {"version": ent.version})JavaScript pattern
import {
UtaError,
UtaTokenError,
UtaPermissionError,
UtaServiceNotEnabledError,
getEntitlement,
} from "usethatapp";
export async function getCurrentPlan(req) {
const token = req.session.utaAccessToken;
if (!token) return { state: "logged-out" };
try {
const ent = await getEntitlement(token);
return { state: "ok", ent };
} catch (e) {
if (e instanceof UtaTokenError) {
// 401: expired/revoked (or signed out). Reconcile and re-auth.
delete req.session.utaAccessToken;
delete req.session.utaSub;
delete req.session.utaIdToken;
return { state: "logged-out" };
}
if (e instanceof UtaServiceNotEnabledError) {
// Hosted sign-in is switched off for this app. Nothing the USER
// does can fix it — alert yourself and degrade. Check this BEFORE
// UtaPermissionError: it is a subclass, so the branch below would
// swallow it into a re-consent loop that can never succeed.
console.error("Hosted sign-in add-on is disabled for this app");
return { state: "ok", ent: null };
}
if (e instanceof UtaPermissionError) {
return { state: "needs-scope" }; // re-consent with openid entitlements
}
if (e instanceof UtaError) {
console.error("getEntitlement failed:", e.message);
return { state: "ok", ent: null }; // degrade to free
}
throw e;
}
}Catching Everything
If you only care about "did the SDK fail?" without distinguishing reasons, catch the base class:
Python
from usethatapp import UtaError, get_entitlement
try:
ent = get_entitlement(access_token)
except UtaError as e:
logger.warning("entitlement check failed: %s", e)
ent = NoneJavaScript
import { UtaError, getEntitlement } from "usethatapp";
try {
ent = await getEntitlement(accessToken);
} catch (e) {
if (e instanceof UtaError) {
console.warn("entitlement check failed:", e.message);
ent = null;
} else {
throw e;
}
}For production we recommend catching the specific subclasses above — at minimum, UtaTokenError needs its own handler so the user gets re-authenticated (or reconciled after sign-out) instead of a generic error page.
Next Steps
See Sign in with UseThatApp for the full flow, and the framework-specific guides (Flask, FastAPI, Express, Next.js, and others) for framework-idiomatic handling.
