UseThatApp Logo
    JavaScript

    JavaScript Documentation

    Sell your JavaScript web app from your own website — one platform for purchasing (UseThatApp is the merchant of record: hosted buy links plus a live pricing API), licensing (live entitlements), and authentication ("Sign in with UseThatApp" over OAuth 2.0 / OpenID Connect). A buyer who purchases on your site lands back in your app signed in, with the entitlement already live — sign-in plus the live plan is how you gate features. The usethatapp npm package is a confidential, server-side client — it holds your client secret and validates ID tokens, so it runs on your Node server, never in the browser. Each framework guide below uses the tested example from the package — copy, paste, and ship.

    Read the protocol first

    The OpenID Connect page covers the model these guides build on — the authorization-code flow, the pairwise sub, and the entitlement API. The framework pages here are the concrete wiring.

    This guide covers the Hosted sign-in path

    It has UseThatApp run login, so buyers return signed in and the same session answers what they bought — the Hosted sign-in add-on. If your app already has its own accounts, you do not need any of the sign-in wiring below: keep your login and verify purchases with a license key your server validates in one call, included in the per-sale fee.

    What the SDK does

    A single OIDC login gives you both identity (a privacy-preserving, per-app sub) and access to the entitlement API (the user's live plan). Three phases:

    1. Sign in. Start a login with beginLogin(), send the user to UseThatApp to authenticate, and finish in your callback with completeLogin(). You get a UtaSession carrying the user's pairwise sub and OAuth tokens.
    2. Read the plan. Call getEntitlement(accessToken) whenever you gate features. Always authoritative — a canceled license stops being entitled immediately.
    3. Offer the upgrade. When the user isn't entitled, send them to hosted checkout with purchaseUrl(priceId) — UseThatApp is the merchant of record (payment, tax, refunds), and the buyer returns signed in with the entitlement already live, no webhooks. getPrices() renders your live pricing page; manageUrl() links entitled users to subscription management.

    Installation

    terminal
    npm install usethatapp

    These docs describe 2.2.0 or newer; if you are upgrading from an earlier release, run npm install usethatapp@latest first — the License Key API, product_id, and UtaServiceNotEnabledError do not exist before it.

    Prerequisites

    • Node.js 18+, server-side only. The SDK uses the global fetch plus Node's node:crypto and node:fs — it does not run on Edge runtimes (Cloudflare Workers, Vercel Edge, Next.js middleware.ts) or in a browser bundle. Do entitlement checks in a Node server route.
    • ESM-only. Set "type": "module" in package.json or use the .mjs extension. require("usethatapp") from CommonJS is not supported — from a CJS file, use dynamic import: const uta = await import("usethatapp");
    • An OAuth client registered on your app's Integration page — see OpenID Connect for registering your redirect URI and copying credentials.

    Configuration

    The SDK reads from process.env by default; override any setting programmatically with configure({...}). Only the client id, secret, and redirect URI are required; the rest default to production. Keep the secret server-side.

    environment
    UTA_CLIENT_ID=...            # required
    UTA_CLIENT_SECRET=...        # required (server-side only)
    UTA_REDIRECT_URI=https://yourapp.com/callback
    # optional — default to production:
    UTA_ISSUER=https://www.usethatapp.com/o
    UTA_API_URL=https://www.usethatapp.com
    UTA_SCOPES="openid entitlements"

    If your hosting provider mounts secrets as files, set UTA_CLIENT_SECRET_PATH instead of UTA_CLIENT_SECRET and the SDK reads the secret from that file.

    Public API

    Everything imports from "usethatapp". All calls are async and return Promises.

    usethatapp
    import {
      beginLogin,      // () => { authorizationUrl, flowState }
      completeLogin,   // ({ code, state, flowState }) => UtaSession
      getEntitlement,  // (accessToken) => Entitlement
      refresh,         // (refreshToken) => UtaSession
      userinfo,        // (accessToken) => { sub }
      logoutUrl,       // ({ idToken, postLogoutRedirectUri }) => string
    
      // Selling — hosted checkout & live pricing
      purchaseUrl,     // (priceId, { next, ref, email }?) => string — pure URL builder
      manageUrl,       // ({ next }?) => string — subscription management, pure
      getPrices,       // () => Promise<AppPrices> — live prices, no auth
      getAppInfo,      // () => Promise<AppInfo> — public app profile, no auth
    
      configure,       // override env config
      resetConfig,
    
      // Errors (all extend UtaError)
      UtaError,
      UtaConfigError,
      UtaDiscoveryError,
      UtaAuthError,
      UtaTokenError,       // 401
      UtaPermissionError,  // 403
      UtaServerError,
    
      // Types (TypeScript)
      type UtaSession,
      type Entitlement,
      type UtaFlowState,
      type AppInfo,
      type AppPrices,
      type UtaPrice,
    } from "usethatapp";

    UtaSession and Entitlement

    completeLogin() returns a UtaSession — the user's pairwise pseudonymous sub plus tokens. Key your user records off sub, never an email (none is shared). getEntitlement() returns an Entitlement — gate on the stable product_public_id, display the mutable version.

    usethatapp/types
    interface UtaSession {
      readonly sub: string;                  // pairwise, stable per-app — your local user key
      readonly access_token: string;         // pass to getEntitlement
      readonly expires_at: number;           // unix seconds
      readonly refresh_token: string | null; // null when no refresh token was granted
      readonly id_token: string | null;      // pass to logoutUrl
      readonly scope: string;
      readonly token_type: string;
      readonly claims: Record<string, unknown>; // validated ID-token claims
    }
    
    interface Entitlement {
      readonly entitled: boolean;            // may the user use the app
      readonly version: string | null;       // plan display name, e.g. "Pro" — mutable
      readonly product_id: string | null;    // stable prod_... id — gate features on this
      readonly product_public_id: string | null; // same prod_... value — equal-valued alias
      readonly status: string;               // active / trialing / free / preview / none / ...
      readonly is_free: boolean;
      readonly period_end: string | null;
      readonly raw: Record<string, unknown>; // the full decoded response
    }

    AppPrices, UtaPrice, and AppInfo

    The selling surface. getPrices() returns your app's live prices — never hardcode them: each price carries a ready-made buy_url and the product_id that matches the entitlement’s product_public_id you gate on. Both GET helpers are anonymous (no login); a 404 (app unknown, unpublished, or external sales not enabled) raises UtaError, and a 429 rate limit raises a retriable UtaServerError. See the Pricing API and Sell from your own website.

    usethatapp/types
    interface AppPrices {
      readonly client_id: string;
      readonly app_name: string;
      readonly has_free_tier: boolean;
      readonly prices: readonly UtaPrice[]; // readonly — copy with [...prices] before sorting
      readonly raw: Record<string, unknown>;
    }
    
    interface UtaPrice {
      readonly public_id: string;      // prc_... — pass to purchaseUrl()
      readonly product_id: string;     // matches the entitlement's product_public_id
      readonly product_name: string;
      readonly amount: string;         // decimal string, e.g. "10.00"
      readonly currency: string;       // lowercase ISO code, e.g. "usd"
      readonly is_recurring: boolean;
      readonly frequency: string | null; // "day" | "week" | "month" | "year"; null = one-time
      readonly buy_url: string;        // ready-made hosted-checkout buy link
    }
    
    interface AppInfo {
      readonly client_id: string;
      readonly name: string;
      readonly tagline: string;
      readonly listing_mode: string;   // "marketplace" or "external"
      readonly url: string;
      readonly marketplace_url: string;
      readonly raw: Record<string, unknown>;
    }

    Quickstart — any framework

    usethatapp
    import { beginLogin, completeLogin, getEntitlement } from "usethatapp";
    
    // 1) Start login — however your framework spells "redirect":
    const { authorizationUrl, flowState } = await beginLogin();
    saveToSession("utaFlow", flowState);        // JSON-serializable
    res.redirect(authorizationUrl);
    
    // 2) In your callback (reads ?code=...&state=... off the request).
    //    On cancel/deny the provider sends ?error=... and no code — handle it first:
    if (req.query.error) return res.redirect("/");   // login was canceled
    const session = await completeLogin({
      code: req.query.code,
      state: req.query.state,
      flowState: loadFromSession("utaFlow"),
    });
    saveToSession("utaSub", session.sub);
    saveToSession("utaAccessToken", session.access_token);
    
    // 3) Anywhere you gate features:
    const ent = await getEntitlement(loadFromSession("utaAccessToken"));
    if (ent.entitled && ent.product_public_id === "prod_<your-pro-id>") { /* ... */ }

    ⚠ Browser apps need a backend.

    The SDK is a confidential client — it must never ship to the browser. A single-page app (React, Vue) runs the SDK in a small backend (a "backend-for-frontend") and calls that from the browser. See the React guide for the pattern.

    Choose your framework

    Each guide is a complete walkthrough with the tested example from the SDK.

    Next steps

    Review Error Handling for the typed errors getEntitlement raises, and Gotchas for the edge cases.

    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.