UseThatApp Logo

    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:

    exception hierarchy
    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 raise UtaAuthError (the state didn't match, or the user canceled) or UtaTokenError (the authorization code couldn't be exchanged, or the returned ID token failed validation). Discovery problems surface as UtaDiscoveryError.
    • Entitlement — get_entitlement(access_token) / getEntitlement(accessToken) can raise UtaTokenError (401), UtaPermissionError (403), UtaServerError (5xx), or a plain UtaError (400 — the client isn't linked to an app).
    • Pricing / app info — get_prices() / getPrices() and get_app_info() / getAppInfo() hit the anonymous public API and can raise a plain UtaError (404 — the app is unknown, unpublished, or external sales isn't enabled for it yet) or UtaServerError (429 rate limit — 120 requests/minute per IP — or 5xx/network; both retriable).
    • URL builders — purchase_url / purchaseUrl and manage_url / manageUrl make no network calls. They only fail locally: an empty price id (Python ValueError, JavaScript UtaError), or a missing UTA_CLIENT_ID for the manage URL (UtaConfigError).

    Errors at a Glance

    ExceptionTriggerSourceRecommended handling
    UtaConfigErrorMissing or invalid config (no UTA_CLIENT_ID/UTA_CLIENT_SECRET/UTA_REDIRECT_URI)Local, at first SDK callLog + fail the request with HTTP 500. This is a deploy-time bug; fix the env config and redeploy.
    UtaDiscoveryErrorCouldn't fetch the OpenID discovery document or JWKS from the issuerLogin / entitlementUsually a network blip or a misconfigured UTA_ISSUER. Retry; if it persists, check outbound connectivity to usethatapp.com.
    UtaAuthErrorThe state didn't match (CSRF/cookie issue), or the user canceled / denied consentcomplete_loginTreat 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.
    UtaTokenErrorHTTP 401 — the access token is missing, expired, or revoked; or, at login, the code exchange / ID-token validation failedget_entitlement / complete_loginRe-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.
    UtaPermissionErrorHTTP 403 — a valid token that lacks the entitlements scopeget_entitlementRe-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_entitlementLog + 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 yetget_prices / get_app_infoCheck the UTA_CLIENT_ID, then the readiness list on the app's manage page: seller verified, app published, product active, external sales on.
    UtaServerErrorHTTP 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_keyRetry 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 the entitlements scope. 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

    any view that gates on the plan
    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

    any handler that gates on the plan
    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

    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 = None

    JavaScript

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

    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.