Purchases
How checkout works, and how to take it over.
Beta
The Web SDK is in beta and its API may change between releases.
The default: Superwall handles it
Create an instance without a purchaseController and the SDK runs the standard checkout itself: Stripe checkout from the paywall, ?code= redemption on return, and polling for the resulting entitlements.
const sw = createSuperwall({ apiKey: "pk_…" });Products come from the paywall you configured in the dashboard. Subscription status updates on its own once the purchase settles.
Reading the catalog
const products = await sw.purchases.getProducts();Products come from the parsed config. Expect an empty array before configuration lands, or when the config carries no products.
Restoring
await sw.purchases.restore();Discount codes
When a paywall is on screen, sw.activePaywall exposes controls for Stripe promotion codes.
const active = sw.activePaywall.value;
if (active) {
const outcome = await active.redeemDiscount("LAUNCH20");
if (!outcome.valid) console.log("rejected:", outcome.reason);
}Redeeming validates the code against the checkout backend, re-prices the paywall's Stripe products, and forwards the code to every Stripe web checkout session created afterwards, including rebuilding sessions that were already prefetched.
active.clearDiscount();clearDiscount is fire-and-forget and returns void. The paywall sends no acknowledgement, so there is nothing to await.
Behavior to know:
redeemDiscountresolves after roughly ten seconds with{ valid: false, reason: "timeout" }if the paywall never replies, which happens when it has no Stripe products loaded.- A second
redeemDiscountwhile one is in flight supersedes the first, which settles as{ valid: false, reason: "superseded" }.clearDiscount()settles an in-flight redemption the same way. - It rejects with a
DiscountErrorfor an empty or whitespace-only code, when no paywall is presented (including a staleactivePaywallhandle from a paywall that has since been dismissed), and when the presenter has no message channel. - A discount does not survive dismissal. Re-apply it after each presentation.
reasonis an open union. Known values includecode_not_found,code_invalid,no_valid_products,no_applicable_products,error,timeout,superseded, andpaywall_dismissed. Do not write an exhaustiveswitchover it.
This is the programmatic equivalent of the Apply Discount tap action in the paywall editor. See Discount codes for setting the code up in Stripe.
Running your own checkout
To bill through your own system while keeping the SDK's paywalls and placements, push the outcome in yourself:
sw.purchases.setSubscriptionStatus({
status: "ACTIVE",
entitlements: [
{ id: "pro", type: "SERVICE_LEVEL", isActive: true, productIds: [] },
],
});The SDK treats what you push as truth for client-side state. It is still not a security boundary. See Server-side gating.
Taking over the controller
For full control of the transaction, pass a purchaseController at creation. It replaces the built-in automatic controller entirely, including ?code= redemption and entitlements polling, so you own those too.
const sw = createSuperwall({
apiKey: "pk_…",
purchaseController: myPurchaseController,
});There is no runtime setter. The controller is wired once, at createSuperwall.
How is this guide?