UseThatApp Logo

    License Keys

    License keys let you sell through UseThatApp while keeping your own authentication — Auth0, Clerk, Supabase, homegrown, anything. Every purchase mints one key. Your server validates it with one call and unlocks what that purchase bought. Your users never see a UseThatApp login.

    Which mode should I use?

    License keys (this page) if your app already has accounts. Included in the base per-sale fee, nothing to enable. Hosted sign-in if you would rather not run login at all — we handle sign-in, passwords, and recovery, and the entitlement answers from the same session. The two modes coexist on the same licenses: turning one on never breaks the other.

    Configure

    The License Key API arrived in SDK 2.2.0. Upgrading from an earlier release? Run pip install --upgrade usethatapp or npm install usethatapp@latest before you start — the calls on this page are not in 2.1.x.

    A keys-mode integration needs exactly two values — your app's OAuth client id and secret, both on your dashboard under Manage app → Integration. The SDK signs every license call with them. No redirect URI, no end-user token, nothing else to set up.

    .env (server-side only)
    UTA_CLIENT_ID=your-client-id
    UTA_CLIENT_SECRET=your-client-secret
    # Pointing at a non-production instance? Also set this — it
    # defaults to production:
    # UTA_API_URL=http://localhost:8000

    Keep the secret on your server. These calls are server-to-server; never ship the secret to a browser or a desktop binary.

    Validate a key

    The call you make wherever you gate paid functionality. Keys are opaque key_… strings, scoped to one app.

    Python

    your gate
    from usethatapp import validate_license_key
    
    PRO = "prod_<your-pro-id>"
    
    state = validate_license_key(user.license_key)
    if state.entitled and state.product_public_id == PRO:
        unlock_pro_features()
    else:
        offer_upgrade()

    JavaScript

    your gate
    import { validateLicenseKey } from "usethatapp";
    
    const PRO = "prod_<your-pro-id>";
    
    const state = await validateLicenseKey(user.licenseKey);
    if (state.entitled && state.product_public_id === PRO) {
      unlockProFeatures();
    } else {
      offerUpgrade();
    }

    Gate on the prod_… id

    Branch on the opaque prod_… identifier — carried by both product_id and product_public_id, which are equal-valued aliases — never on a plan display name. It survives plan renames, and it is the same value the dashboard, the MCP list_products tool, and the Pricing API give you. If a subscription moves to a sibling plan, this field reports the plan the buyer holds now.

    What you get back

    All three calls return the same LicenseState.

    FieldMeaning
    entitledTrue if this key may use the app right now. Gate on this.
    statusWhy — see the table below. Use it to word your UI honestly.
    product_id / product_public_idThe prod_… plan the buyer holds now. Equal-valued aliases — the same pair the entitlement endpoint carries, so gating code works identically in both modes.
    license_idStable id for this license — what you pass to regeneration.
    period_endISO date the paid period ends, or null. Cache against this.
    canceled_atISO date of a terminal cancellation, or null.
    license_keyKey material — set only by get_order and regenerate_license_key, the two calls that hand you a key.

    Link a purchase to your own user

    When a buyer returns from hosted checkout, the redirect carries an opaque uta_order reference. Exchange it for the license key and attach it to whichever of your users is signed in — no email matching, no asking anyone to paste a key.

    Python

    your return route
    from usethatapp import (
        get_order, UtaOrderProcessingError,
    )
    
    # Buyer lands on /thanks?uta_order=ord_7bQ...
    try:
        order = get_order(request.GET["uta_order"])
    except UtaOrderProcessingError:
        # Paid, license not landed yet. Retry shortly.
        return render("checkout_pending.html")
    
    user.license_key = order.license_key
    user.save()

    JavaScript

    your return route
    import {
      getOrder, UtaOrderProcessingError,
    } from "usethatapp";
    
    // Buyer lands on /thanks?uta_order=ord_7bQ...
    let order;
    try {
      order = await getOrder(req.query.uta_order);
    } catch (e) {
      if (e instanceof UtaOrderProcessingError) {
        return res.render("checkout-pending");
      }
      throw e;
    }
    
    await saveLicenseKey(user, order.license_key);

    The reference is a signed, expiring handle scoped to your app — not a secret, and useless without your client secret. Store the license_key it returns; that is what you validate from then on.

    Selling to desktop or CLI users with no return redirect? Have the buyer paste the key instead — it is shown on the checkout success page, in the receipt email, and any time on their buyer portal.

    Statuses

    StatusentitledMeaning
    activetrueSubscription in good standing.
    trialingtrueIn a trial period.
    one_time_activetrueA one-time (non-recurring) purchase.
    past_due / unpaidfalseA renewal failed and we are retrying the card. Recoverable — the same key returns to active on its own if payment succeeds. Prompt them; don't delete their data.
    pausedfalseTemporarily suspended; may resume.
    canceledfalseTerminal: cancellation ran its course, dunning was exhausted, or the purchase was refunded.

    Cancellation is not instant lockout

    A buyer who cancels keeps entitled: true until period_end — they paid for that period. Only a refund revokes immediately. Cache against period_end and you get this behavior for free.

    Errors

    The SDKs raise typed errors; a canceled or refunded purchase is not one of them — it returns normally with entitled=False, because a key you once accepted deserves a lifecycle answer rather than an exception.

    ErrorWhen
    UtaNotFoundError
    code unknown_key
    A key we never issued, or one belonging to another app. Means "this string is not a license" — not "expired".
    UtaNotFoundError
    code unknown_order
    An order reference that is unverifiable, expired, or not yours.
    UtaNotFoundError
    code unknown_license
    Regeneration for a license id that isn't your app's.
    UtaOrderProcessingErrorPayment cleared, license not landed yet. Retry for a few seconds — show "finishing up", not an error.
    UtaLicenseCanceledErrorYou tried to regenerate a key whose license is terminally gone.
    UtaConfigErrorMissing UTA_CLIENT_ID / UTA_CLIENT_SECRET, or credentials rejected.

    Rate limits: 120/min on validation and order lookup, 30/min on regeneration — bucketed per caller IP plus client id, so another app's traffic (or a stranger guessing secrets) can never spend your quota. A tripped limit answers 429 with a Retry-After header and raises UtaServerError — back off and retry. Full exception hierarchy: Error Handling.

    Regenerating a key

    A compromise kill switch, not a recovery path. It rotates by license_id, returns the new key once in license_key, and the old key stops working at that instant — so only you can call it, because your backend is what holds the key. Deliver the replacement to your customer yourself.

    Python

    revoking a leaked key
    from usethatapp import regenerate_license_key
    
    state = regenerate_license_key(user.license_id)
    user.license_key = state.license_key
    user.save()
    email_new_key(user, state.license_key)

    JavaScript

    revoking a leaked key
    import { regenerateLicenseKey } from "usethatapp";
    
    const state = await regenerateLicenseKey(user.licenseId);
    await saveLicenseKey(user, state.license_key);
    await emailNewKey(user, state.license_key);

    If a buyer has merely lost their key, do not rotate — send them to their buyer portal, where the existing key is always re-viewable. Rotating would break the key your own app still has on file. Every rotation is audit-logged.

    Staying current

    Verification is pull: re-validate on your own cadence and cache the answer until period_end. There are no outbound webhooks today, so cancellation, dunning, and refunds surface as a status change the next time you ask. For a user-present gate (someone opens your app, you check), that is all you need.

    Going further

    • Not on Python or JavaScript? All three calls are plain HTTPS with HTTP Basic client credentials — request, response, and error tables are in the REST API reference.
    • Where do buy links come from? Sell from your own website covers the checkout half of this flow.
    • Money questions stay with us. Card updates, cancellation, invoices, and refunds live on the hosted buyer portal — link to it rather than rebuilding it.
    • Prefer we run login too? Hosted sign-in replaces key storage with a session-scoped entitlement call.

    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.