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.
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:8000Keep 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
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
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.
| Field | Meaning |
|---|---|
entitled | True if this key may use the app right now. Gate on this. |
status | Why — see the table below. Use it to word your UI honestly. |
product_id / product_public_id | The 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_id | Stable id for this license — what you pass to regeneration. |
period_end | ISO date the paid period ends, or null. Cache against this. |
canceled_at | ISO date of a terminal cancellation, or null. |
license_key | Key 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
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
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
| Status | entitled | Meaning |
|---|---|---|
active | true | Subscription in good standing. |
trialing | true | In a trial period. |
one_time_active | true | A one-time (non-recurring) purchase. |
past_due / unpaid | false | A 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. |
paused | false | Temporarily suspended; may resume. |
canceled | false | Terminal: 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.
| Error | When |
|---|---|
UtaNotFoundErrorcode unknown_key | A key we never issued, or one belonging to another app. Means "this string is not a license" — not "expired". |
UtaNotFoundErrorcode unknown_order | An order reference that is unverifiable, expired, or not yours. |
UtaNotFoundErrorcode unknown_license | Regeneration for a license id that isn't your app's. |
UtaOrderProcessingError | Payment cleared, license not landed yet. Retry for a few seconds — show "finishing up", not an error. |
UtaLicenseCanceledError | You tried to regenerate a key whose license is terminally gone. |
UtaConfigError | Missing 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
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
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.
