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:

  • redeemDiscount resolves 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 redeemDiscount while 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 DiscountError for an empty or whitespace-only code, when no paywall is presented (including a stale activePaywall handle 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.
  • reason is an open union. Known values include code_not_found, code_invalid, no_valid_products, no_applicable_products, error, timeout, superseded, and paywall_dismissed. Do not write an exhaustive switch over 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?

On this page