Quick Start
Go from zero to selling your app from your own website — with UseThatApp as the merchant of record handling payment, tax, and refunds. One integration covers the whole loop: live prices and Buy buttons on your site, hosted checkout, the buyer returning to your app, and feature gating on what they bought. The usethatapp SDK runs server-side and does the crypto for you.
One choice before you start
How should your app verify a purchase? If it already has accounts, keep them and validate a license key — included in the per-sale fee, and you can skip every sign-in step below. This Quick Start walks the other path, the Hosted sign-in add-on, where we run login and the same session answers what they bought. Selling works the same either way.
The Loop You're Building
- Sell. Your website renders live prices from the pricing API and links each Buy button to hosted checkout — a purchase link on usethatapp.com.
- Return. Checkout creates the license synchronously and sends the buyer to your registered Login URL, where your
/loginroute signs them in over OIDC — no webhooks, no polling. - Gate. Anywhere it matters, read the live plan with
get_entitlement/getEntitlementand branch on the stableproduct_public_id. (Keys mode callsvalidate_license_keyhere instead and branches on the same field.)
Prerequisites
- Python 3.9+ or Node.js 18+
- An app registered on usethatapp.com
- Your app's Client ID and Client Secret, from the Integration page of your UseThatApp dashboard
Step 1 — Install the Package
Python
pip install usethatappJavaScript
npm install usethatappStep 2 — Register Your App
Open your app's Integration page on the UseThatApp dashboard and register:
- A Redirect URI — your OIDC callback URL, matched exactly (an allow-list). It must be HTTPS on your domain (
localhostis allowed for development), e.g.https://yourapp.com/callback. - A Login URL — the page in your app that starts sign-in. This matters twice: marketplace launches land here, and it's the default place buyers return to after checkout — so make sure it signs them straight in.
Then copy your Client ID and Client Secret. The secret is shown once — keep it on your server, never in the browser.
Step 3 — Configure Environment Variables
Both SDKs read configuration from environment variables, and both accept in-code overrides via configure(...) (Python also reads Django settings). Only the client id, secret, and redirect URI are required — the rest default to production:
UTA_CLIENT_ID=... # required
UTA_CLIENT_SECRET=... # required (server-side only)
UTA_REDIRECT_URI=https://yourapp.com/callback # required, must match the dashboard
# optional — default to production:
UTA_ISSUER=https://www.usethatapp.com/o
UTA_API_URL=https://www.usethatapp.com
UTA_SCOPES="openid entitlements"Keep the client secret out of version control. If your host mounts secrets as files (Render Secret Files, Fly.io, Kubernetes, GCP Secret Manager, etc.) set UTA_CLIENT_SECRET_PATH instead. See the Hosting Provider guide for per-provider recommendations. (The purchase helpers are lighter: purchase_url needs no config at all, and manage_url / get_prices only need UTA_CLIENT_ID — so a separate marketing site can use them without the OAuth secrets.)
Step 4 — Add Login and Callback Routes
Expose a /login route that starts sign-in and a /callback route (matching UTA_REDIRECT_URI) that completes it. These routes serve both marketplace launches and buyers returning from checkout. The SDK is framework-agnostic — you wire three small bits: read the callback query params, store flow_state in your session, and issue the redirects.
Python (Flask)
from flask import Flask, redirect, request, session, url_for
from usethatapp import (
UtaError,
begin_login,
complete_login,
get_entitlement,
)
app = Flask(__name__)
app.secret_key = "change-me"
@app.route("/login")
def login():
auth_url, flow_state = begin_login()
session["uta_flow"] = flow_state
return redirect(auth_url)
@app.route("/callback")
def callback():
# On cancel/deny, OAuth redirects back with ?error=... and no code.
if request.args.get("error"):
session.pop("uta_flow", None)
return redirect(url_for("home"))
try:
s = complete_login(
code=request.args.get("code"),
state=request.args.get("state"),
flow_state=session.pop("uta_flow", {}),
)
except UtaError as e:
return f"login failed: {e}", 400
session["uta_sub"] = s.sub
session["uta_access_token"] = s.access_token
return redirect(url_for("home"))
@app.route("/")
def home():
token = session.get("uta_access_token")
if not token:
return '<a href="/login">Log in with UseThatApp</a>'
ent = get_entitlement(token)
return {"sub": session.get("uta_sub"), "version": ent.version}JavaScript (Express)
import express from "express";
import session from "express-session";
import { beginLogin, completeLogin, getEntitlement, UtaError } from "usethatapp";
const app = express();
app.use(session({ secret: "change-me", resave: false, saveUninitialized: true }));
app.get("/login", async (req, res, next) => {
try {
const { authorizationUrl, flowState } = await beginLogin();
req.session.utaFlow = flowState;
res.redirect(authorizationUrl);
} catch (e) { next(e); }
});
app.get("/callback", async (req, res) => {
// On cancel/deny, OAuth redirects back with ?error=... and no code.
if (req.query.error) {
delete req.session.utaFlow;
return res.redirect("/");
}
try {
const s = await completeLogin({
code: req.query.code,
state: req.query.state,
flowState: req.session.utaFlow,
});
delete req.session.utaFlow;
req.session.utaSub = s.sub;
req.session.utaAccessToken = s.access_token;
res.redirect("/");
} catch (e) {
res.status(e instanceof UtaError ? 400 : 500).send(`login failed: ${e.message}`);
}
});
app.get("/", async (req, res) => {
const token = req.session.utaAccessToken;
if (!token) return res.send('<a href="/login">Log in with UseThatApp</a>');
const ent = await getEntitlement(token);
res.json({ sub: req.session.utaSub, version: ent.version });
});
app.listen(3000);Step 5 — Gate on the Entitlement
Anywhere you gate paid functionality, read the live plan and check the stable product_public_id (never the mutable display version). The entitlement is authoritative the moment a purchase completes — and the moment a license is canceled:
Python
from usethatapp import get_entitlement
ent = get_entitlement(access_token)
# Entitlement(entitled, version, product_public_id, status, is_free, period_end)
if ent.entitled and ent.product_public_id == "prod_<your-pro-id>":
unlock_pro_features()JavaScript
import { getEntitlement } from "usethatapp";
const ent = await getEntitlement(accessToken);
// { entitled, version, product_public_id, status, is_free, period_end }
if (ent.entitled && ent.product_public_id === "prod_<your-pro-id>") {
unlockProFeatures();
}Step 6 — Create Your Products and Prices
On the dashboard, create your products (tiers) and their prices — recurring or one-time. Each product gets the stable product_public_id your gates branch on, and each price gets an opaque public id (prc_…) plus a ready-made buy_url, both served by the Pricing API.
Step 7 — Put Prices and Buy Buttons on Your Site
Render your pricing page from the live pricing API — never hardcode prices — and point each Buy button at the hosted checkout. Buyers get one page for email + country + ZIP, card entry via Stripe, then land back in your app entitled:
Python
from usethatapp import get_prices, purchase_url
plans = get_prices() # live — no auth needed
for p in plans.prices:
# p.product_name, p.amount ("10.00"), p.currency ("usd"),
# p.frequency ("month" or None), p.buy_url (hosted checkout)
render_plan(p, buy=purchase_url(p.public_id,
next="https://yourapp.com/welcome"))JavaScript
import { getPrices, purchaseUrl } from "usethatapp";
const { prices } = await getPrices(); // live — no auth needed
for (const p of prices) {
// p.product_name, p.amount ("10.00"), p.currency ("usd"),
// p.frequency ("month" or null), p.buy_url (hosted checkout)
renderPlan(p, purchaseUrl(p.public_id,
{ next: "https://yourapp.com/welcome" }));
}next is optional — leave it off and buyers return to your Login URL. It must be HTTPS on your registered domain. The pricing API is also public and CORS-enabled, so a static marketing page can fetch it straight from the browser — see Sell from your own website.
Step 8 — Test the Funnel, Then Go Live
- Run the seller walkthrough. Open your own buy link signed in as your developer account: same pages, a test banner, no Stripe calls, no charge — and it ends by setting your developer-preview entitlement (
status: "preview") so you can verify the return into your app end to end. See Test your purchase flow. - Check the readiness list on your app's manage page. A buy link resolves only while your seller account is verified, the app is published, the product is active, and external sales is on (the default); otherwise it returns 404.
- Go live. Buy links work for buyers in the United States, the European Union, Canada, Australia and the other countries offered at checkout.
Next Steps
That's the whole loop. To go deeper: Sell from your own website (purchasing), Licensing (entitlements), and Sign in with UseThatApp (identity and sign-out). Then pick your stack:
