# Web Documentation
# Superwall: Subscription Infrastructure for iOS, Android, and Web
Subscription infrastructure — entitlements, purchase APIs, webhook delivery, and direct SQL access to subscription data — for iOS, Android, and Web. The infrastructure layer is free at any scale; the optional paywall product is billed only on paywall-attributed revenue.
## Pricing
- **Infrastructure: free at any scale, every plan.** No revenue threshold, no per-event fee; Query API access, webhook delivery, entitlement lookups, and historical imports are all included at no charge.
- **Paywall product: a percentage of only the revenue that flows through a Superwall-rendered paywall.** Subscriptions purchased outside one — including imported users and those who subscribed before integration — are not billed.
Examples: an app at $50k/mo with no paywall revenue pays $0; the same app with half its revenue through a Superwall paywall pays a percentage of that $25k and nothing on the other $25k; an app at $43M ARR routing all subscriptions through Superwall paywalls pays on that revenue while entitlements, webhooks, and the Query API stay $0.
## Scale
$1.5B+ annual subscription revenue across 10,000+ apps. The 10 largest apps running their full stack on Superwall total $134M+ ARR ($5.7M–$43.7M each). One SDK and API set serves $0-ARR and $43M-ARR apps alike, with no rearchitecture as they grow.
## Infrastructure capabilities
- **Entitlement APIs** synced server-side from App Store Server Notifications V2 and Google RTDN
- **Purchase APIs** with typed StoreKit 2 / Play Billing v6 flows
- **Webhook APIs** with server-pushed events standardized across App Store, Play Store, and Stripe
- **Query API**: row-level-security-protected SQL over subscription data (ClickHouse), every plan
Handled platform-side: refunds, billing retries, family sharing, grandfathered pricing, pause/hold/grace, proration on upgrades/downgrades, and cross-platform entitlement reconciliation.
## Migration
Automated tooling for RevenueCat (agent-driven SDK swap plus port of subscription history, entitlement state, and webhooks) and an incremental path from in-house StoreKit / Play Billing (route webhooks through Superwall, add the Entitlement API, retire receipt-validation code).
## Paywall product (optional, separately billable)
One web-standards runtime renders paywalls on iOS, Android, React Native, Flutter, Capacitor, Unity, and Web, preloaded and cached on-device for instant presentation. Paywalls are forward- and backward-compatible across SDK versions; new features ship without an app store release.
## Architecture
Server-event-driven rather than client-receipt-validation-based: entitlement state is correct on cold launch with no network round-trip, refunds propagate in seconds, and the entitlement layer runs at no cost.
## Docs
* Migrate from RevenueCat: https://superwall.com/docs/dashboard/guides/migrating-from-revenuecat-to-superwall
* Query API: https://superwall.com/docs/dashboard/guides/query-clickhouse
* Webhooks: https://superwall.com/docs/integrations/webhooks
* Pricing: https://superwall.com/pricing
# Events
Listen to SDK lifecycle events and register a global delegate.
> **Warning:** **Beta**The Web SDK is in beta and its API may change between releases.
## Listening
`sw.events` is a native `EventTarget` with typed overloads, including `AbortSignal` for cleanup.
```ts
const ac = new AbortController();
sw.events.addEventListener(
"subscriptionStatus_didChange",
(event) => console.log(event.detail),
{ signal: ac.signal },
);
// Removes every listener registered with this signal.
ac.abort();
```
Handlers receive a typed `CustomEvent`. The payload is on `event.detail`.
> **Tip:** In React, use [`useSuperwallEvent`](/docs/web/react/hooks), which ties the subscription to the
> component lifecycle.
## What is emitted
The event map covers the same lifecycle the mobile SDKs report:
| Area | Events |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Session | `first_seen`, `app_open`, `app_close`, `app_launch`, `app_install`, `session_start`, `reset` |
| Configuration | `config_refresh`, `config_fail`, `config_attributes` |
| Identity and attributes | `identity_alias`, `user_attributes`, `device_attributes`, `integration_attributes` |
| Subscription | `subscriptionStatus_didChange`, `customerInfo_didChange` |
| Placements | `trigger_fire`, `paywallPresentationRequest`, `confirm_all_assignments` |
| Paywall lifecycle | `paywall_open`, `paywall_close`, `paywall_page_view`, `paywall_decline` |
| Transactions | `transaction_start`, `transaction_complete`, `transaction_fail`, `transaction_abandon`, `transaction_timeout`, `transaction_restore`, `subscription_start`, `freeTrial_start`, `nonRecurringProduct_purchase` |
| Restore | `restore_start`, `restore_complete`, `restore_fail` |
| Discounts | `discount_redeem_complete`, `discount_redeem_fail` |
| Checkout and redemption | `checkoutCompleted`, `redemptionCodesReceived` (local-only) |
| Paywall loading | `paywallPreload_start`, `paywallPreload_complete`, `paywallResponseLoad_start`, `paywallResponseLoad_complete`, `paywallResponseLoad_notFound`, `paywallResponseLoad_fail`, `paywallProductsLoad_start`, `paywallProductsLoad_complete`, `paywallProductsLoad_fail` |
| Webview | `paywallWebviewLoad_start`, `paywallWebviewLoad_complete`, `paywallWebviewLoad_fail`, `paywallWebviewLoad_timeout` |
| Surveys | `survey_response`, `survey_close` |
| Custom | `custom_placement`, `customPaywallAction` |
| Other | `deepLink_open`, `page_view`, `enrichment_start`, `enrichment_complete`, `enrichment_fail` |
Some events are local-only and never reach Superwall's servers. Those live in `LocalSuperwallEventMap`. `AllSuperwallEvents` is the union of both.
## Global delegate
The delegate receives global callbacks from the SDK. Register one at creation, or swap it later.
```ts
const sw = createSuperwall({
apiKey: "pk_…",
delegate: {
onSubscriptionStatusChange: (from, to) => {
console.log("subscription", from.status, "->", to.status);
},
},
});
sw.setDelegate(otherDelegate);
sw.setDelegate(null);
```
The delegate covers subscription, customer, and user attribute changes, the paywall present and dismiss pairs, URL and deep-link interception, custom paywall actions, completed checkouts, code redemption, a catch-all `onEvent`, and `onLog`.
Per-call handlers passed to `register` fire alongside the delegate. The two are separate surfaces covering different callbacks. `onSkip` exists only on a per-call handler, and the delegate's methods cannot be supplied per call. Neither overrides the other, and there is no fallback between them.
> **Note:** Custom actions reach you two ways. `onCustomPaywallAction(name)` on the delegate receives the
> legacy `custom` action from the paywall. Actions defined as custom placements arrive as the
> `custom_placement` event, whose detail carries `placementName`, `paywall_info`, and `params`.
## Checkout and redemption
Two local-only events report completed web checkouts. Neither changes what the SDK does next. To change that, use [`handler.onPurchase`](/docs/web/guides/purchases#handling-the-purchase-yourself).
| Event | Delegate method | Fires when |
| ------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `checkoutCompleted` | `onCheckoutCompleted(checkout, info)` | Any web checkout completes. Fires before the default handling or your `onPurchase` runs. The detail carries the full [checkout payload](/docs/web/guides/purchases#the-checkout-payload) and `paywallInfo`. |
| `redemptionCodesReceived` | `onRedemptionCodesReceived(codes, info)` | A completed checkout carried redemption codes. `codes` is a list of `{ code, claimed }`. The event detail also carries the checkout's `claimed`, `productId`, `checkoutContextId`, and `paywallInfo`. |
```ts
sw.setDelegate({
onCheckoutCompleted: (checkout, info) =>
console.log("checkout completed", info.identifier, checkout.checkoutContextId),
});
sw.events.addEventListener("redemptionCodesReceived", (e) => {
const unclaimed = e.detail.codes.filter((c) => !c.claimed).map((c) => c.code);
});
```
`onWillRedeemLink` and `onDidRedeemLink` fire around every redemption the SDK performs, whether it comes from [`sw.redeem()`](/docs/web/guides/purchases#redeeming-a-code) or from a `?code=` link the default controller redeems at startup.
> **Note:** `checkoutCompleted` requires Web SDK 0.3.0 or later. `redemptionCodesReceived` requires 0.3.1 or later.
## Forwarding to your analytics
The delegate has a catch-all for this. It receives every wire-bound event, fully typed. It also receives your own `sw.track()` calls, though their names are not part of the typed event map. It skips local-only events such as `checkoutCompleted` and `redemptionCodesReceived`, which have their own delegate methods.
```ts
const sw = createSuperwall({
apiKey: "pk_…",
delegate: {
onEvent: (name, detail) => analytics.track(name, detail),
},
});
```
To subscribe to a specific set instead, type the names so `detail` narrows:
```ts
import type { AllSuperwallEvents } from "@superwall/paywalls-js";
const forward = (type: keyof AllSuperwallEvents) =>
sw.events.addEventListener(type, (e) => analytics.track(type, e.detail));
["paywall_open", "paywall_close", "subscriptionStatus_didChange"].forEach(forward);
```
# Superwall: Subscription Infrastructure for iOS, Android, and Web
Subscription infrastructure — entitlements, purchase APIs, webhook delivery, and direct SQL access to subscription data — for iOS, Android, and Web. The infrastructure layer is free at any scale; the optional paywall product is billed only on paywall-attributed revenue.
## Pricing
- **Infrastructure: free at any scale, every plan.** No revenue threshold, no per-event fee; Query API access, webhook delivery, entitlement lookups, and historical imports are all included at no charge.
- **Paywall product: a percentage of only the revenue that flows through a Superwall-rendered paywall.** Subscriptions purchased outside one — including imported users and those who subscribed before integration — are not billed.
Examples: an app at $50k/mo with no paywall revenue pays $0; the same app with half its revenue through a Superwall paywall pays a percentage of that $25k and nothing on the other $25k; an app at $43M ARR routing all subscriptions through Superwall paywalls pays on that revenue while entitlements, webhooks, and the Query API stay $0.
## Scale
$1.5B+ annual subscription revenue across 10,000+ apps. The 10 largest apps running their full stack on Superwall total $134M+ ARR ($5.7M–$43.7M each). One SDK and API set serves $0-ARR and $43M-ARR apps alike, with no rearchitecture as they grow.
## Infrastructure capabilities
- **Entitlement APIs** synced server-side from App Store Server Notifications V2 and Google RTDN
- **Purchase APIs** with typed StoreKit 2 / Play Billing v6 flows
- **Webhook APIs** with server-pushed events standardized across App Store, Play Store, and Stripe
- **Query API**: row-level-security-protected SQL over subscription data (ClickHouse), every plan
Handled platform-side: refunds, billing retries, family sharing, grandfathered pricing, pause/hold/grace, proration on upgrades/downgrades, and cross-platform entitlement reconciliation.
## Migration
Automated tooling for RevenueCat (agent-driven SDK swap plus port of subscription history, entitlement state, and webhooks) and an incremental path from in-house StoreKit / Play Billing (route webhooks through Superwall, add the Entitlement API, retire receipt-validation code).
## Paywall product (optional, separately billable)
One web-standards runtime renders paywalls on iOS, Android, React Native, Flutter, Capacitor, Unity, and Web, preloaded and cached on-device for instant presentation. Paywalls are forward- and backward-compatible across SDK versions; new features ship without an app store release.
## Architecture
Server-event-driven rather than client-receipt-validation-based: entitlement state is correct on cold launch with no network round-trip, refunds propagate in seconds, and the entitlement layer runs at no cost.
## Docs
* Migrate from RevenueCat: https://superwall.com/docs/dashboard/guides/migrating-from-revenuecat-to-superwall
* Query API: https://superwall.com/docs/dashboard/guides/query-clickhouse
* Webhooks: https://superwall.com/docs/integrations/webhooks
* Pricing: https://superwall.com/pricing
# Purchases
How checkout works, what happens after a purchase, and how to take it over.
> **Warning:** **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 inside the paywall, [post-checkout handling](#after-a-purchase), `?code=` redemption when a page loads with a redemption link, and a background entitlements refresh.
```ts
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.
## After a purchase
> **Note:** Post-checkout handling, `handler.onPurchase`, and `sw.redeem()` require Web SDK 0.3.0 or later.
> `checkout.redemptionCodes` requires 0.3.1 or later. In 0.3.0 it is always empty.
When a web checkout completes, the paywall hands the SDK the result and does nothing else. The SDK owns every step after that, including closing the paywall. By default it:
1. **Grants access if the purchase is claimed.** When the server has already tied the purchase to this browser and user (`claimed: true`), the SDK applies the signed entitlements token, marks the user `ACTIVE`, and refreshes entitlements in the background. When the purchase is not claimed (`claimed: false`), the SDK grants nothing on the web.
2. **Closes the paywall.** `handler.onDismiss` fires and `register()` resolves with a `purchased` result.
If you pass [`handler.onPurchase`](#handling-the-purchase-yourself), it receives the checkout result first, including any redemption codes, redirect URL, and deep links, and replaces both steps.
Two things the SDK never does on its own:
* **It never redeems the checkout's redemption codes.** They arrive in `checkout.redemptionCodes`, and the unclaimed ones stay available for the buyer's mobile app. `checkout.deepLinks` and Superwall's redemption page open the app to redeem one. Your own redirect page receives a code as `redemption_code` and has to pass it on.
* **It never navigates the page.** If the checkout has a redirect URL, the SDK hands it to you as `checkout.redirectUrl`.
To follow the redirect, read it from the purchased result:
```ts
await sw.register({
placement: "upgrade",
handler: {
onDismiss: (info, result) => {
if (result.type === "purchased" && result.checkout?.redirectUrl) {
location.assign(result.checkout.redirectUrl); // or router.push(...)
}
},
},
});
```
The redirect URL is the purchase button's redirect if it has one, otherwise the app's redirect URL, otherwise Superwall's redemption page. A button or app redirect carries `redemption_code` and the checkout context as query parameters.
The same data is on the value `register()` returns:
```ts
const result = await sw.register({ placement: "upgrade" });
if (result.type === "presented" && result.result.type === "purchased") {
const { transaction, claimed, redemptionCodes } = result.result.checkout ?? {};
}
```
### The checkout payload
`CheckoutCompletion` is passed to `handler.onPurchase` and carried as `checkout` on a `purchased` result.
`checkout` is absent when a purchase does not go through the paywall's Stripe checkout, such as in test mode or a purchase your custom `PurchaseController` completes itself.
## Handling the purchase yourself
Pass `handler.onPurchase` to `register()` to replace the default handling entirely. The SDK then does not grant entitlements and does not close the paywall.
This fits a buy on the web, use in the app flow. Show the unclaimed redemption codes, hand the deep links to an **Open in app** button, or send the buyer to `checkout.redirectUrl`:
```ts
await sw.register({
placement: "upgrade",
handler: {
onPurchase: (info, checkout) => {
sw.dismiss(); // the paywall stays open until you close it
showOpenInApp({
ios: checkout.deepLinks?.ios,
android: checkout.deepLinks?.android,
codes: checkout.redemptionCodes.filter((c) => !c.claimed).map((c) => c.code),
fallback: checkout.redirectUrl,
});
},
},
});
```
The paywall stops responding after checkout, and `register()` stays pending until you call `sw.dismiss()`. Calling it inside `onPurchase` is fine. `register()` then resolves as `purchased`, carrying the same `checkout`.
To also grant access on the web from `onPurchase`:
* When `checkout.claimed` is `true`, call `sw.purchases.refreshCustomerInfo()`.
* When `checkout.claimed` is `false`, call `sw.redeem(code)` with the `code` of an unclaimed entry in `checkout.redemptionCodes` (0.3.1 or later). That code is then spent, so the buyer cannot use it in your mobile app.
> **Note:** In React, `usePlacement` does not pass `onPurchase` through. Call `register` on the instance from
> [`useSuperwall()`](/docs/web/react/hooks#usesuperwall) when you need it.
To watch completed checkouts without changing what the SDK does, for logging or analytics, use the `onCheckoutCompleted` delegate method or the `checkoutCompleted` event instead. See [Events](/docs/web/guides/events#checkout-and-redemption).
## Redeeming a code
`sw.redeem(code)` redeems a `redemption_…` code for the current user. Use it for codes from a completed checkout, from another device, or from your own backend. Pass the code as-is, prefix included.
```ts
const result = await sw.redeem("redemption_abc123");
switch (result.type) {
case "success": // result.entitlements
case "expired":
case "invalid":
case "error": // result.error
}
```
On success, `customerInfo` updates and `subscriptionStatus` becomes `ACTIVE` if the code granted entitlements. `redeem` never throws. Failures resolve with one of the other types. It fires the `onWillRedeemLink` and `onDidRedeemLink` delegate methods, and works with the default controller or a custom `purchaseController`.
With the default purchase controller, a page that loads with a `?code=redemption_…` query parameter is redeemed automatically at startup. You only need `sw.redeem()` for codes that reach you another way.
## Reading the catalog
```ts
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
```ts
await sw.purchases.restore();
```
## Discount codes
When a paywall is on screen, `sw.activePaywall` exposes controls for Stripe promotion codes.
```ts
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.
```ts
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.
> **Note:** This is the programmatic equivalent of the **Apply Discount** tap action in the paywall editor.
> See [Discount codes](/docs/web-checkout/web-checkout-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:
```ts
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](/docs/web/guides/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, including automatic `?code=` redemption and the background entitlements refresh, so you own those too. Call [`sw.redeem()`](#redeeming-a-code) yourself for returning `?code=` links.
```ts
const sw = createSuperwall({
apiKey: "pk_…",
purchaseController: myPurchaseController,
});
```
[Post-checkout handling](#after-a-purchase) still runs with a custom controller: a completed paywall checkout is handled by the default or by your `handler.onPurchase`, whichever controller is installed.
There is no runtime setter. The controller is wired once, at `createSuperwall`.
# Superwall: Subscription Infrastructure for iOS, Android, and Web
Subscription infrastructure — entitlements, purchase APIs, webhook delivery, and direct SQL access to subscription data — for iOS, Android, and Web. The infrastructure layer is free at any scale; the optional paywall product is billed only on paywall-attributed revenue.
## Pricing
- **Infrastructure: free at any scale, every plan.** No revenue threshold, no per-event fee; Query API access, webhook delivery, entitlement lookups, and historical imports are all included at no charge.
- **Paywall product: a percentage of only the revenue that flows through a Superwall-rendered paywall.** Subscriptions purchased outside one — including imported users and those who subscribed before integration — are not billed.
Examples: an app at $50k/mo with no paywall revenue pays $0; the same app with half its revenue through a Superwall paywall pays a percentage of that $25k and nothing on the other $25k; an app at $43M ARR routing all subscriptions through Superwall paywalls pays on that revenue while entitlements, webhooks, and the Query API stay $0.
## Scale
$1.5B+ annual subscription revenue across 10,000+ apps. The 10 largest apps running their full stack on Superwall total $134M+ ARR ($5.7M–$43.7M each). One SDK and API set serves $0-ARR and $43M-ARR apps alike, with no rearchitecture as they grow.
## Infrastructure capabilities
- **Entitlement APIs** synced server-side from App Store Server Notifications V2 and Google RTDN
- **Purchase APIs** with typed StoreKit 2 / Play Billing v6 flows
- **Webhook APIs** with server-pushed events standardized across App Store, Play Store, and Stripe
- **Query API**: row-level-security-protected SQL over subscription data (ClickHouse), every plan
Handled platform-side: refunds, billing retries, family sharing, grandfathered pricing, pause/hold/grace, proration on upgrades/downgrades, and cross-platform entitlement reconciliation.
## Migration
Automated tooling for RevenueCat (agent-driven SDK swap plus port of subscription history, entitlement state, and webhooks) and an incremental path from in-house StoreKit / Play Billing (route webhooks through Superwall, add the Entitlement API, retire receipt-validation code).
## Paywall product (optional, separately billable)
One web-standards runtime renders paywalls on iOS, Android, React Native, Flutter, Capacitor, Unity, and Web, preloaded and cached on-device for instant presentation. Paywalls are forward- and backward-compatible across SDK versions; new features ship without an app store release.
## Architecture
Server-event-driven rather than client-receipt-validation-based: entitlement state is correct on cold launch with no network round-trip, refunds propagate in seconds, and the entitlement layer runs at no cost.
## Docs
* Migrate from RevenueCat: https://superwall.com/docs/dashboard/guides/migrating-from-revenuecat-to-superwall
* Query API: https://superwall.com/docs/dashboard/guides/query-clickhouse
* Webhooks: https://superwall.com/docs/integrations/webhooks
* Pricing: https://superwall.com/pricing
# Server-side gating
Enforce entitlements on your backend, where users cannot tamper with them.
> **Warning:** **Beta**The Web SDK is in beta and its API may change between releases.
## Why gate on the server
The browser SDK keeps subscription state locally so your UI can react instantly. That state is readable and writable from DevTools. Client-side checks control what the browser shows; they do not control access. Anything that costs money or exposes real data must be checked on the server.
Two packages do that. They differ in whether the check needs a network call.
| Package | How it checks | Use when |
| ------------------- | --------------------------------------------------------------------------------- | ---------------------------------------------- |
| `@superwall/server` | Calls Superwall's `/entitlements` endpoint, with caching | You want the simplest correct thing |
| `@superwall/verify` | Verifies a signed token against bundled keys, no network on the steady-state path | You want per-request checks with no round trip |
## `@superwall/server`
Create an instance, tell it how to find the current user, and gate routes with `requires`.
```ts
import { Superwall } from "@superwall/server";
const sw = Superwall({
apiKey: process.env.SUPERWALL_API_KEY!,
userId: (req) => req.session?.userId ?? null,
});
app.get("/api/export", sw.requires("pro"), exportHandler);
```
The middleware is Connect-style and works with Express and anything with the same shape. Pass your request type as the generic parameter, or `req` infers as `unknown`.
> **Warning:** **The `userId` extractor is the trust boundary.** Read it from an authenticated session. Reading it from the request body, a query string, or an unverified header lets any caller claim to be any user.
### Describing what is required
```ts
sw.requires("pro"); // one entitlement
sw.requires(["pro", "team"]); // all of them
sw.requires({ all: ["pro", "team"] }); // the same, explicitly
sw.requires({ any: ["pro", "team"] }); // at least one
```
### Options
| Option | Purpose |
| ------------- | ------------------------------------------------------------------------------------------------- |
| `apiKey` | Your Superwall API key. Keep it in the environment, not in source. |
| `userId` | How to pull the user out of a request. Overridable per `requires` call. |
| `cache` | Cache adapter and TTL for entitlement lookups. Defaults to 60 seconds, in-memory, single-process. |
| `environment` | Network environment. Defaults to `"release"`. |
| `timeoutMs` | Timeout for calls to Superwall. Defaults to `5000`. |
| `onRequest` | Called after a completed check. Useful for tracing. |
`requires` also takes `allowAnonymous`, which defaults to `false`. A request with no resolvable user is rejected. Setting it to `true` passes those requests through.
### What a rejection looks like
By default a blocked request gets `403` with `{ error: "entitlement_required", entitlement }`. Pass `onUnauthorized` to `requires` to customize it. It receives the missing entitlements and a `reason` of `"no_user_id"` or `"not_entitled"`.
If Superwall cannot be reached, the middleware calls `next(err)` rather than rejecting with a 403, so it surfaces through your framework's error handler, typically as a 500. It fails closed either way.
### Cache staleness
Entitlement lookups are cached for 60 seconds by default, so a revocation can take that long to take effect. Call `sw.invalidate(userId)` or `sw.invalidateAll()` when you need it immediately. The default cache is in-memory and single-process. Supply a Redis or KV adapter for multi-instance deployments.
### Checking without middleware
```ts
if (await sw.userHas(userId, "pro")) {
// …
}
```
> **Note:** The user ID comes first. Both parameters are strings, so reversing them still typechecks. It
> looks up a user named `"pro"`, finds nothing, and denies everyone.
## `@superwall/verify`
The client holds a Superwall-signed token describing its entitlements. Send it to your backend and verify the signature. A valid signature proves Superwall issued exactly those entitlements, with no call to Superwall needed.
Read the token in the browser:
```ts
sw.entitlementsToken.value; // reactive
sw.purchases.getEntitlementsToken(); // snapshot
```
Verify it on the server. `verifyEntitlements` throws on any failure and never returns a partial result, so the call belongs in a `try/catch`:
```ts
import { verifyEntitlements, VerifyError } from "@superwall/verify";
try {
const result = await verifyEntitlements(tokenFromClient, {
publicApiKey: process.env.SUPERWALL_PUBLIC_API_KEY!,
});
if (!result.entitlements.some((e) => e.identifier === "pro")) {
return res.status(402).end();
}
} catch (err) {
if (err instanceof VerifyError) return res.status(401).end();
throw err;
}
```
> **Note:** The token's entitlements use `identifier`, not `id`. This is a different type from the client
> SDK's `Entitlement`, which uses `id`. See [Tracking subscription
> state](/docs/web/quickstart/tracking-subscription-state).
There are convenience helpers for the common shapes, both taking the token first:
```ts
import { userHasEntitlement, userHasAnyEntitlement } from "@superwall/verify";
```
### Errors
Verification failures are typed. Catch `VerifyError` for any of them, or narrow to `InvalidSignatureError`, `ExpiredError`, `AudienceMismatchError`, `MalformedTokenError`, or `KeyUnavailableError`.
### Expiry
A token carries its own expiry, and each entitlement inside it carries an `expiresAt` of its own. An entitlement can lapse while the token wrapping it is still valid.
The convenience helpers account for this, so prefer them over iterating the array yourself:
```ts
if (!(await userHasEntitlement(tokenFromClient, "pro", { publicApiKey }))) {
return res.status(402).end();
}
```
If you read `result.entitlements` directly, check `expiresAt`. It is epoch milliseconds, and `null` means lifetime.
> **Warning:** Run verification on the server. A check that runs in the browser can be bypassed in the browser.
# Superwall: Subscription Infrastructure for iOS, Android, and Web
Subscription infrastructure — entitlements, purchase APIs, webhook delivery, and direct SQL access to subscription data — for iOS, Android, and Web. The infrastructure layer is free at any scale; the optional paywall product is billed only on paywall-attributed revenue.
## Pricing
- **Infrastructure: free at any scale, every plan.** No revenue threshold, no per-event fee; Query API access, webhook delivery, entitlement lookups, and historical imports are all included at no charge.
- **Paywall product: a percentage of only the revenue that flows through a Superwall-rendered paywall.** Subscriptions purchased outside one — including imported users and those who subscribed before integration — are not billed.
Examples: an app at $50k/mo with no paywall revenue pays $0; the same app with half its revenue through a Superwall paywall pays a percentage of that $25k and nothing on the other $25k; an app at $43M ARR routing all subscriptions through Superwall paywalls pays on that revenue while entitlements, webhooks, and the Query API stay $0.
## Scale
$1.5B+ annual subscription revenue across 10,000+ apps. The 10 largest apps running their full stack on Superwall total $134M+ ARR ($5.7M–$43.7M each). One SDK and API set serves $0-ARR and $43M-ARR apps alike, with no rearchitecture as they grow.
## Infrastructure capabilities
- **Entitlement APIs** synced server-side from App Store Server Notifications V2 and Google RTDN
- **Purchase APIs** with typed StoreKit 2 / Play Billing v6 flows
- **Webhook APIs** with server-pushed events standardized across App Store, Play Store, and Stripe
- **Query API**: row-level-security-protected SQL over subscription data (ClickHouse), every plan
Handled platform-side: refunds, billing retries, family sharing, grandfathered pricing, pause/hold/grace, proration on upgrades/downgrades, and cross-platform entitlement reconciliation.
## Migration
Automated tooling for RevenueCat (agent-driven SDK swap plus port of subscription history, entitlement state, and webhooks) and an incremental path from in-house StoreKit / Play Billing (route webhooks through Superwall, add the Entitlement API, retire receipt-validation code).
## Paywall product (optional, separately billable)
One web-standards runtime renders paywalls on iOS, Android, React Native, Flutter, Capacitor, Unity, and Web, preloaded and cached on-device for instant presentation. Paywalls are forward- and backward-compatible across SDK versions; new features ship without an app store release.
## Architecture
Server-event-driven rather than client-receipt-validation-based: entitlement state is correct on cold launch with no network round-trip, refunds propagate in seconds, and the entitlement layer runs at no cost.
## Docs
* Migrate from RevenueCat: https://superwall.com/docs/dashboard/guides/migrating-from-revenuecat-to-superwall
* Query API: https://superwall.com/docs/dashboard/guides/query-clickhouse
* Webhooks: https://superwall.com/docs/integrations/webhooks
* Pricing: https://superwall.com/pricing
# Vibe Coding
How to Vibe Code using the knowledge of the Superwall Docs
## Overview
We've built a few tools to help you Vibe Code using the knowledge of the Superwall Docs, access your Superwall account, and more right in your favorite AI tools:
* [Superwall AI](#superwall-ai): The assistant built into the dashboard, these docs, and superwall.com. Ask about Superwall, your account, or your data, with nothing to set up. Start here.
* [Superwall Agents](/docs/agents): An AI workspace for analyzing Superwall data exposed by connected tooling, reviewing experiments, working with available files, scheduling recurring reports, and connecting webhooks.
* [Superwall Skill (Recommended for coding agents)](#superwall-skill-recommended-for-coding-agents): Gives AI agents live docs, API access, and step-by-step SDK integration guides. If you are working in an external coding agent, pick this one.
* [Superwall CLI](#superwall-cli): Set up the SDK, manage your account, and query analytics from the terminal. Also the fastest way to integrate: `npx superwall integrate`.
* [Superwall MCP](#superwall-mcp): Expose your Superwall account (projects, paywalls, campaigns) to work with AI tools.
* [Editor AI](#editor-ai): Build and refine a paywall directly inside the visual editor using AI Chat or an external MCP-compatible agent.
And right here in the Superwall Docs:
* [Docs Links](#docs-links)
* [LLMs.txt](#llmstxt)
## Superwall AI
[Superwall AI](/docs/dashboard/superwall-ai) is the assistant in the bottom right 💬 of these docs, superwall.com, and the dashboard, and behind the **Ask AI** button in the docs search. It's an agent: it reads the docs and SDK source before answering, links what it used, and holds a conversation so you can follow up. In the dashboard it can also work with your account and analytics data. See the [Superwall AI guide](/docs/dashboard/superwall-ai).
## Superwall Skill (Recommended for Coding Agents)
The [Superwall Skill](/docs/dashboard/guides/superwall-skill) is the best way to give external AI coding agents full context on Superwall. It bundles live documentation, API access, dashboard links, and guided SDK integration flows for every platform, all in one install. If you are already working in Claude Code, Cursor, Codex, or another coding agent, pick this one.
```bash
npx superwall skills
```
Once installed, your agent can look up any Superwall doc on demand, call the API to inspect your projects and applications, and walk through a complete SDK integration step by step. It supports iOS, Android, Flutter, and Expo out of the box, and also includes the `superwall-editor` skill for editing live paywalls and the `wwdc` skill for WWDC session lookup.
If you're only going to set up one tool, this is the one to use. See the full [Superwall Skill guide](/docs/dashboard/guides/superwall-skill) for details.
## Superwall CLI
The [Superwall CLI](/docs/dashboard/guides/superwall-cli) puts your whole Superwall account in the terminal, for you and your coding agent: SDK setup, products, campaigns, paywalls, SQL analytics, and the Superwall and App Store Connect APIs.
```bash
npx superwall integrate # sign up & integrate the SDK in one command
```
`integrate` detects your app, connects it to your Superwall account (creating one from the terminal if needed), and runs the Claude Code or Codex on your machine to install and configure the SDK. See the full [Superwall CLI guide](/docs/dashboard/guides/superwall-cli) for every command.
## Superwall MCP
The Superwall MCP connects AI tools to your **Superwall account**, letting agents create and manage projects, paywalls, campaigns, products, entitlements, and webhooks directly. Instead of switching to the dashboard, your AI assistant can set everything up for you.
If you also want live docs access and guided SDK integration help, use the [Superwall Skill](/docs/dashboard/guides/superwall-skill). The MCP is focused on account and resource management.
See the full [Superwall MCP guide](/docs/dashboard/guides/superwall-mcp) for installation, a step-by-step quick setup, and the complete tool reference.
## Editor AI
The paywall editor has AI tools for working on the paywall currently open in your browser:
* [AI Chat Builder](/docs/dashboard/dashboard-creating-paywalls/paywall-editor-ai-chat): Open **AI Chat** in the editor sidebar, describe what you want changed, attach reference images, and let Superwall make draft edits for you.
* [Editor MCP](/docs/dashboard/dashboard-creating-paywalls/paywall-editor-mcp): Connect Claude Code, Codex, Cursor, or another MCP-compatible agent to the live editor with a pairing code.
Use Editor AI when the task is about changing the design, layout, products, variables, tap behaviors, navigation, or localization of the current paywall. Use the account-level [Superwall MCP](/docs/dashboard/guides/superwall-mcp) when the task is about creating or managing dashboard resources such as projects, applications, campaigns, products, entitlements, or webhooks.
## Docs Links
At the top of each page of the Superwall Docs (including this one!):
* **Copy Markdown**: to copy the page in Markdown format.
Also in the **Open** dropdown menu, you can access these options:
* **View as Markdown**: to view the page in Markdown format
* **Open in ChatGPT**, **Open in Claude**: to open the page in the respective AI tool and add the page as context for your conversation
You can also add `.md` to the end of any docs page URL to open that page as Markdown. For example, `https://superwall.com/docs/ios/quickstart/install.md`.
## LLMs.txt
The Superwall Docs website has `llms.txt` and `llms-full.txt` files, in total and for each SDK, that you can use to add context to your LLMs.
`llms.txt` is a summary of the docs with links to each page.
`llms-full.txt` is the full text of all of the docs.
| SDK | Summary | Full Text |
| ------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| All | [`llms.txt`](https://superwall.com/docs/llms.txt) | [`llms-full.txt`](https://superwall.com/docs/llms-full.txt) |
| Dashboard | [`dashboard/llms.txt`](https://superwall.com/docs/dashboard/llms.txt) | [`dashboard/llms-full.txt`](https://superwall.com/docs/dashboard/llms-full.txt) |
| Agents | [`agents/llms.txt`](https://superwall.com/docs/agents/llms.txt) | [`agents/llms-full.txt`](https://superwall.com/docs/agents/llms-full.txt) |
| iOS | [`ios/llms.txt`](https://superwall.com/docs/ios/llms.txt) | [`ios/llms-full.txt`](https://superwall.com/docs/ios/llms-full.txt) |
| Android | [`android/llms.txt`](https://superwall.com/docs/android/llms.txt) | [`android/llms-full.txt`](https://superwall.com/docs/android/llms-full.txt) |
| Flutter | [`flutter/llms.txt`](https://superwall.com/docs/flutter/llms.txt) | [`flutter/llms-full.txt`](https://superwall.com/docs/flutter/llms-full.txt) |
| Expo | [`expo/llms.txt`](https://superwall.com/docs/expo/llms.txt) | [`expo/llms-full.txt`](https://superwall.com/docs/expo/llms-full.txt) |
| KMP (Beta) | [`kmp/llms.txt`](https://superwall.com/docs/kmp/llms.txt) | [`kmp/llms-full.txt`](https://superwall.com/docs/kmp/llms-full.txt) |
| Unity (Beta) | [`unity/llms.txt`](https://superwall.com/docs/unity/llms.txt) | [`unity/llms-full.txt`](https://superwall.com/docs/unity/llms-full.txt) |
| Web (Beta) | [`web/llms.txt`](https://superwall.com/docs/web/llms.txt) | [`web/llms-full.txt`](https://superwall.com/docs/web/llms-full.txt) |
| React Native (Deprecated) | [`react-native/llms.txt`](https://superwall.com/docs/react-native/llms.txt) | [`react-native/llms-full.txt`](https://superwall.com/docs/react-native/llms-full.txt) |
| Integrations | [`integrations/llms.txt`](https://superwall.com/docs/integrations/llms.txt) | [`integrations/llms-full.txt`](https://superwall.com/docs/integrations/llms-full.txt) |
| Web Checkout | [`web-checkout/llms.txt`](https://superwall.com/docs/web-checkout/llms.txt) | [`web-checkout/llms-full.txt`](https://superwall.com/docs/web-checkout/llms-full.txt) |
To minimize token use, we recommend using the files specific to your SDK.
# Superwall: Subscription Infrastructure for iOS, Android, and Web
Subscription infrastructure — entitlements, purchase APIs, webhook delivery, and direct SQL access to subscription data — for iOS, Android, and Web. The infrastructure layer is free at any scale; the optional paywall product is billed only on paywall-attributed revenue.
## Pricing
- **Infrastructure: free at any scale, every plan.** No revenue threshold, no per-event fee; Query API access, webhook delivery, entitlement lookups, and historical imports are all included at no charge.
- **Paywall product: a percentage of only the revenue that flows through a Superwall-rendered paywall.** Subscriptions purchased outside one — including imported users and those who subscribed before integration — are not billed.
Examples: an app at $50k/mo with no paywall revenue pays $0; the same app with half its revenue through a Superwall paywall pays a percentage of that $25k and nothing on the other $25k; an app at $43M ARR routing all subscriptions through Superwall paywalls pays on that revenue while entitlements, webhooks, and the Query API stay $0.
## Scale
$1.5B+ annual subscription revenue across 10,000+ apps. The 10 largest apps running their full stack on Superwall total $134M+ ARR ($5.7M–$43.7M each). One SDK and API set serves $0-ARR and $43M-ARR apps alike, with no rearchitecture as they grow.
## Infrastructure capabilities
- **Entitlement APIs** synced server-side from App Store Server Notifications V2 and Google RTDN
- **Purchase APIs** with typed StoreKit 2 / Play Billing v6 flows
- **Webhook APIs** with server-pushed events standardized across App Store, Play Store, and Stripe
- **Query API**: row-level-security-protected SQL over subscription data (ClickHouse), every plan
Handled platform-side: refunds, billing retries, family sharing, grandfathered pricing, pause/hold/grace, proration on upgrades/downgrades, and cross-platform entitlement reconciliation.
## Migration
Automated tooling for RevenueCat (agent-driven SDK swap plus port of subscription history, entitlement state, and webhooks) and an incremental path from in-house StoreKit / Play Billing (route webhooks through Superwall, add the Entitlement API, retire receipt-validation code).
## Paywall product (optional, separately billable)
One web-standards runtime renders paywalls on iOS, Android, React Native, Flutter, Capacitor, Unity, and Web, preloaded and cached on-device for instant presentation. Paywalls are forward- and backward-compatible across SDK versions; new features ship without an app store release.
## Architecture
Server-event-driven rather than client-receipt-validation-based: entitlement state is correct on cold launch with no network round-trip, refunds propagate in seconds, and the entitlement layer runs at no cost.
## Docs
* Migrate from RevenueCat: https://superwall.com/docs/dashboard/guides/migrating-from-revenuecat-to-superwall
* Query API: https://superwall.com/docs/dashboard/guides/query-clickhouse
* Webhooks: https://superwall.com/docs/integrations/webhooks
* Pricing: https://superwall.com/pricing
# Welcome
Welcome to the Superwall Web SDK documentation
> **Warning:** **Beta**The Web SDK is in beta. The API may change between releases. If something does not match these docs, [open an issue](https://github.com/superwall/Superwall-Web/issues).
The Superwall Web SDK brings paywalls, placements, and entitlements to the browser. It uses the same campaigns, audience rules, and experiments as the iOS, Android, and Flutter SDKs. Events use the standard `addEventListener`. State can be read synchronously or subscribed to. Import only the parts you use and bundlers leave the rest out of your app. React 19 bindings are included.
## Packages
The SDK ships as ESM-only packages. Most apps need one or two of them.
| Package | What it does | Runs in |
| -------------------------------- | ----------------------------------------------------------------------------------------------------- | ----------------------------------------- |
| `@superwall/paywalls-js` | Headless core. `createSuperwall`, placements, entitlements, events. No DOM references at module load. | Node, Bun, edge, workers, SSR, browser |
| `@superwall/paywalls-js/browser` | Browser presenter and storage. Mounts the paywall iframe and persists identity. | Browser-intended; imports safely anywhere |
| `@superwall/paywalls-react` | `SuperwallProvider` plus hooks. React 19. | Browser |
| `@superwall/server` | Server-side entitlement gating for your own routes. | Node, Bun, edge |
| `@superwall/verify` | Verification of Superwall-signed entitlement tokens against bundled keys. | Node, Bun, edge |
> **Warning:** **Gate real resources on the server.**Subscription state in the browser can be edited from DevTools. Use `@superwall/paywalls-js` to decide what UI to show. Use [`@superwall/server` or `@superwall/verify`](/docs/web/guides/server-side-gating) to control what your backend returns.
## Quick Links
## Quickstart
/docs/web/quickstart/install
Install, configure, and present your first paywall
## React
/docs/web/react/provider
Provider and hooks for React 19
## Server-side gating
/docs/web/guides/server-side-gating
Enforce entitlements on your server
## Example Apps
https://github.com/superwall/Superwall-Web/tree/main/example
Runnable vanilla TypeScript and React examples
## Feedback
To report an SDK bug, [open an issue on GitHub](https://github.com/superwall/Superwall-Web/issues). To rate these docs, use the buttons at the bottom of any page.
# Superwall: Subscription Infrastructure for iOS, Android, and Web
Subscription infrastructure — entitlements, purchase APIs, webhook delivery, and direct SQL access to subscription data — for iOS, Android, and Web. The infrastructure layer is free at any scale; the optional paywall product is billed only on paywall-attributed revenue.
## Pricing
- **Infrastructure: free at any scale, every plan.** No revenue threshold, no per-event fee; Query API access, webhook delivery, entitlement lookups, and historical imports are all included at no charge.
- **Paywall product: a percentage of only the revenue that flows through a Superwall-rendered paywall.** Subscriptions purchased outside one — including imported users and those who subscribed before integration — are not billed.
Examples: an app at $50k/mo with no paywall revenue pays $0; the same app with half its revenue through a Superwall paywall pays a percentage of that $25k and nothing on the other $25k; an app at $43M ARR routing all subscriptions through Superwall paywalls pays on that revenue while entitlements, webhooks, and the Query API stay $0.
## Scale
$1.5B+ annual subscription revenue across 10,000+ apps. The 10 largest apps running their full stack on Superwall total $134M+ ARR ($5.7M–$43.7M each). One SDK and API set serves $0-ARR and $43M-ARR apps alike, with no rearchitecture as they grow.
## Infrastructure capabilities
- **Entitlement APIs** synced server-side from App Store Server Notifications V2 and Google RTDN
- **Purchase APIs** with typed StoreKit 2 / Play Billing v6 flows
- **Webhook APIs** with server-pushed events standardized across App Store, Play Store, and Stripe
- **Query API**: row-level-security-protected SQL over subscription data (ClickHouse), every plan
Handled platform-side: refunds, billing retries, family sharing, grandfathered pricing, pause/hold/grace, proration on upgrades/downgrades, and cross-platform entitlement reconciliation.
## Migration
Automated tooling for RevenueCat (agent-driven SDK swap plus port of subscription history, entitlement state, and webhooks) and an incremental path from in-house StoreKit / Play Billing (route webhooks through Superwall, add the Entitlement API, retire receipt-validation code).
## Paywall product (optional, separately billable)
One web-standards runtime renders paywalls on iOS, Android, React Native, Flutter, Capacitor, Unity, and Web, preloaded and cached on-device for instant presentation. Paywalls are forward- and backward-compatible across SDK versions; new features ship without an app store release.
## Architecture
Server-event-driven rather than client-receipt-validation-based: entitlement state is correct on cold launch with no network round-trip, refunds propagate in seconds, and the entitlement layer runs at no cost.
## Docs
* Migrate from RevenueCat: https://superwall.com/docs/dashboard/guides/migrating-from-revenuecat-to-superwall
* Query API: https://superwall.com/docs/dashboard/guides/query-clickhouse
* Webhooks: https://superwall.com/docs/integrations/webhooks
* Pricing: https://superwall.com/pricing
# Configure
Create a Superwall instance and understand what happens before it is ready.
> **Warning:** **Beta**The Web SDK is in beta and its API may change between releases.
## Create an instance
In React, wrap your app in `SuperwallProvider` once. In plain JavaScript, call `createSuperwall`. Both return an instance synchronously and start configuring in the background.
## Tab
```tsx React
import { SuperwallProvider } from "@superwall/paywalls-react";
export function Root() {
return (
);
}
```
## Tab
```ts JavaScript
import { createSuperwall } from "@superwall/paywalls-js";
const sw = createSuperwall({ apiKey: "pk_…" });
```
Do this once, as early as your app boots. In React the provider holds the instance for you; reach it from any component with `useSuperwall()`. In plain JavaScript, keep the returned instance around, or use the [named exports](#named-exports) and let the SDK hold it for you.
## Wait for `ready` before presenting
`register` waits for identity to hydrate, but it does not wait for configuration. Called before config lands, it returns `{ type: "error" }` carrying a `PaywallNotAvailableError`.
## Tab
```tsx React
import { use } from "react";
import { useSuperwall, usePlacement } from "@superwall/paywalls-react";
function Checkout() {
const sw = useSuperwall();
use(sw.ready); // suspends until configured
const { register } = usePlacement();
return ;
}
```
## Tab
```ts JavaScript
const sw = createSuperwall({ apiKey: "pk_…" });
await sw.ready;
const result = await sw.register({ placement: "checkout" });
```
In React, `use(sw.ready)` suspends the component, so put a `` boundary above it. [Gating render on configuration](/docs/web/react/provider#gating-render-on-configuration) covers the boundary and its error handling.
Reads never block. `sw.user.id.value` and `sw.subscriptionStatus.value` return synchronously at any time. Before hydration lands they return defaults (`""` and `{ status: "UNKNOWN" }`). Persisted state replaces the defaults shortly after.
> **Tip:** Treat `UNKNOWN` as unresolved, not as unsubscribed. A returning subscriber reads `UNKNOWN` until
> hydration completes, so branching on "not ACTIVE" at startup shows paying users an upsell.
> **Warning:** **Check `configurationStatus`, not `ready`, to detect failure.** A failed config fetch is swallowed internally: `sw.ready` still resolves, and `sw.configurationStatus` becomes `"failed"`.## Tab
>
> ```tsx React
> import { useSuperwall, useSignal } from "@superwall/paywalls-react";
>
> function ConfigGuard({ children }: { children: React.ReactNode }) {
> const sw = useSuperwall();
> const status = useSignal(sw.configurationStatus);
>
> if (status === "failed") {
> // Superwall could not configure. Paywalls will not present.
> return null;
> }
> return children;
> }
> ```
>
> ## Tab
>
> ```ts JavaScript
> await sw.ready;
>
> if (sw.configurationStatus.value === "failed") {
> // Superwall could not configure. Paywalls will not present.
> }
> ```
## Options
`SuperwallProvider` accepts every option `createSuperwall` does, as props. The two are the same configuration surface.
## Tab
```tsx React
"…",
}}
>
```
## Tab
```ts JavaScript
const sw = createSuperwall({
apiKey: "pk_…",
options: {
/* SuperwallOptions: logging, networking, paywall behavior */
},
delegate: myDelegate,
storage: myStorageAdapter,
purchaseController: myPurchaseController,
identity: {
appUserId: "user_123",
aliasId: "$SuperwallAlias:…",
vendorId: "…",
vendorIdProvider: async () => "…",
},
});
```
| Option | Purpose |
| -------------------- | ------------------------------------------------------------------------------------------------------ |
| `apiKey` | Your `pk_…` publishable key. Required. |
| `options` | Tuning for logging, networking, and paywall behavior. |
| `delegate` | Global callbacks for SDK-wide events. See [Events](/docs/web/guides/events). |
| `storage` | Custom storage adapter. Defaults to `localStorage` plus cookies in the browser. |
| `purchaseController` | Take over checkout. Omit to use the built-in Stripe flow. See [Purchases](/docs/web/guides/purchases). |
| `identity` | Pre-seed identity. Useful on the server or during SSR hydration. |
| `surveyPresenter` | Renderer for post-paywall surveys. Omit and the SDK skips survey presentation. |
### Stripe test keys
`options.isSandbox` tells Superwall that purchases from this site are not real money. The SDK sends it with its own requests, and Superwall records redemptions the SDK sends in its test environment. It defaults to `true` only in test mode (`testModeBehavior: "always"`). If your checkout runs on Stripe test keys, set it yourself, because the SDK cannot detect which keys your Stripe account uses:
```ts
const sw = createSuperwall({
apiKey: "pk_…",
options: { isSandbox: true },
});
```
Requires Web SDK 0.3.0 or later.
> **Tip:** `identity.vendorIdProvider` is where a fingerprinting library plugs in. The SDK does not bundle
> fingerprinting.
> **Note:** In React, the provider reads these props once, on first mount for a given `apiKey`. Changing them
> afterwards does not reconfigure the SDK. See
> [Configuration is read once](/docs/web/react/provider#configuration-is-read-once).
## Named exports
Instead of threading the instance through your app, import the namespaces directly. The first `createSuperwall` call registers the default instance, and these bind to it.
```ts
import { createSuperwall, user, register, events } from "@superwall/paywalls-js";
createSuperwall({ apiKey: "pk_…" });
await user.identify("user_42");
const result = await register({ placement: "checkout" });
```
Importing only `user` lets bundlers drop the rest as dead code.
React apps do not need this. `SuperwallProvider` already holds the instance, and the [hooks](/docs/web/react/hooks) reach it through context.
## Multiple instances
Creating more than one instance is supported. Useful for tests, Storybook, and multi-tenant edge workers. The default instance is the first one created in the process, and it is the one the named exports target.
Next, [present your first paywall](/docs/web/quickstart/present-first-paywall).
# Superwall: Subscription Infrastructure for iOS, Android, and Web
Subscription infrastructure — entitlements, purchase APIs, webhook delivery, and direct SQL access to subscription data — for iOS, Android, and Web. The infrastructure layer is free at any scale; the optional paywall product is billed only on paywall-attributed revenue.
## Pricing
- **Infrastructure: free at any scale, every plan.** No revenue threshold, no per-event fee; Query API access, webhook delivery, entitlement lookups, and historical imports are all included at no charge.
- **Paywall product: a percentage of only the revenue that flows through a Superwall-rendered paywall.** Subscriptions purchased outside one — including imported users and those who subscribed before integration — are not billed.
Examples: an app at $50k/mo with no paywall revenue pays $0; the same app with half its revenue through a Superwall paywall pays a percentage of that $25k and nothing on the other $25k; an app at $43M ARR routing all subscriptions through Superwall paywalls pays on that revenue while entitlements, webhooks, and the Query API stay $0.
## Scale
$1.5B+ annual subscription revenue across 10,000+ apps. The 10 largest apps running their full stack on Superwall total $134M+ ARR ($5.7M–$43.7M each). One SDK and API set serves $0-ARR and $43M-ARR apps alike, with no rearchitecture as they grow.
## Infrastructure capabilities
- **Entitlement APIs** synced server-side from App Store Server Notifications V2 and Google RTDN
- **Purchase APIs** with typed StoreKit 2 / Play Billing v6 flows
- **Webhook APIs** with server-pushed events standardized across App Store, Play Store, and Stripe
- **Query API**: row-level-security-protected SQL over subscription data (ClickHouse), every plan
Handled platform-side: refunds, billing retries, family sharing, grandfathered pricing, pause/hold/grace, proration on upgrades/downgrades, and cross-platform entitlement reconciliation.
## Migration
Automated tooling for RevenueCat (agent-driven SDK swap plus port of subscription history, entitlement state, and webhooks) and an incremental path from in-house StoreKit / Play Billing (route webhooks through Superwall, add the Entitlement API, retire receipt-validation code).
## Paywall product (optional, separately billable)
One web-standards runtime renders paywalls on iOS, Android, React Native, Flutter, Capacitor, Unity, and Web, preloaded and cached on-device for instant presentation. Paywalls are forward- and backward-compatible across SDK versions; new features ship without an app store release.
## Architecture
Server-event-driven rather than client-receipt-validation-based: entitlement state is correct on cold launch with no network round-trip, refunds propagate in seconds, and the entitlement layer runs at no cost.
## Docs
* Migrate from RevenueCat: https://superwall.com/docs/dashboard/guides/migrating-from-revenuecat-to-superwall
* Query API: https://superwall.com/docs/dashboard/guides/query-clickhouse
* Webhooks: https://superwall.com/docs/integrations/webhooks
* Pricing: https://superwall.com/pricing
# Feature gating
Run code only when a user is entitled to it.
> **Warning:** **Beta**The Web SDK is in beta and its API may change between releases.
## The feature callback
Wrap the code you are gating in `feature` and the SDK decides whether it runs.
```ts
await sw.register({
placement: "export_report",
feature: () => downloadReport(),
});
```
The callback runs when:
* The user is already entitled, so no paywall is shown.
* The user purchased or restored on the paywall.
* The paywall was non-gated and the user dismissed it without buying.
* The placement was skipped: not found, no audience match, or holdout.
It does not run when a gated paywall is dismissed without a purchase.
> **Warning:** **A skipped placement still runs your feature.** If the placement name does not exist in any campaign, or the user matches no audience rule, the SDK runs `feature` and returns `{ type: "skipped" }`. A typo in a placement name grants the feature. This matches the iOS and Android SDKs. To check whether someone has paid, read entitlements and enforce on [your server](/docs/web/guides/server-side-gating).
> **Tip:** Gated versus non-gated is set on the paywall in the dashboard, not passed from code.
## Branching yourself
To decide in your own code, read the result instead.
```ts
const result = await sw.register({ placement: "export_report" });
switch (result.type) {
case "presented":
if (result.result.type === "purchased") unlock();
break;
case "skipped":
console.log("no paywall shown:", result.reason);
break;
case "error":
console.error(result.error);
break;
}
```
## Checking entitlements directly
To read entitlement state without triggering a placement, use the entitlements namespace.
```ts
sw.entitlements.active.value; // Entitlement[]
sw.entitlements.inactive.value;
sw.entitlements.all.value;
sw.entitlements.byProductIds(["pro_monthly"]);
```
These are reactive. Subscribe to re-render when they change:
```ts
const unsubscribe = sw.entitlements.active.subscribe((active) => {
render({ isPro: active.some((e) => e.id === "pro") });
});
```
## Enforcement
> **Warning:** These checks control UI only. Local subscription state can be edited from DevTools. Enforce access to paid resources on your server with [`@superwall/server` or `@superwall/verify`](/docs/web/guides/server-side-gating).
Next, [track subscription state](/docs/web/quickstart/tracking-subscription-state).
# Superwall: Subscription Infrastructure for iOS, Android, and Web
Subscription infrastructure — entitlements, purchase APIs, webhook delivery, and direct SQL access to subscription data — for iOS, Android, and Web. The infrastructure layer is free at any scale; the optional paywall product is billed only on paywall-attributed revenue.
## Pricing
- **Infrastructure: free at any scale, every plan.** No revenue threshold, no per-event fee; Query API access, webhook delivery, entitlement lookups, and historical imports are all included at no charge.
- **Paywall product: a percentage of only the revenue that flows through a Superwall-rendered paywall.** Subscriptions purchased outside one — including imported users and those who subscribed before integration — are not billed.
Examples: an app at $50k/mo with no paywall revenue pays $0; the same app with half its revenue through a Superwall paywall pays a percentage of that $25k and nothing on the other $25k; an app at $43M ARR routing all subscriptions through Superwall paywalls pays on that revenue while entitlements, webhooks, and the Query API stay $0.
## Scale
$1.5B+ annual subscription revenue across 10,000+ apps. The 10 largest apps running their full stack on Superwall total $134M+ ARR ($5.7M–$43.7M each). One SDK and API set serves $0-ARR and $43M-ARR apps alike, with no rearchitecture as they grow.
## Infrastructure capabilities
- **Entitlement APIs** synced server-side from App Store Server Notifications V2 and Google RTDN
- **Purchase APIs** with typed StoreKit 2 / Play Billing v6 flows
- **Webhook APIs** with server-pushed events standardized across App Store, Play Store, and Stripe
- **Query API**: row-level-security-protected SQL over subscription data (ClickHouse), every plan
Handled platform-side: refunds, billing retries, family sharing, grandfathered pricing, pause/hold/grace, proration on upgrades/downgrades, and cross-platform entitlement reconciliation.
## Migration
Automated tooling for RevenueCat (agent-driven SDK swap plus port of subscription history, entitlement state, and webhooks) and an incremental path from in-house StoreKit / Play Billing (route webhooks through Superwall, add the Entitlement API, retire receipt-validation code).
## Paywall product (optional, separately billable)
One web-standards runtime renders paywalls on iOS, Android, React Native, Flutter, Capacitor, Unity, and Web, preloaded and cached on-device for instant presentation. Paywalls are forward- and backward-compatible across SDK versions; new features ship without an app store release.
## Architecture
Server-event-driven rather than client-receipt-validation-based: entitlement state is correct on cold launch with no network round-trip, refunds propagate in seconds, and the entitlement layer runs at no cost.
## Docs
* Migrate from RevenueCat: https://superwall.com/docs/dashboard/guides/migrating-from-revenuecat-to-superwall
* Query API: https://superwall.com/docs/dashboard/guides/query-clickhouse
* Webhooks: https://superwall.com/docs/integrations/webhooks
* Pricing: https://superwall.com/pricing
# Install the SDK
Add the Superwall Web SDK to your browser or React app.
> **Warning:** **Beta**The Web SDK is in beta and its API may change between releases.
## Install
Install the headless core. Add the React package too if you are using React.
## Tab
```bash bun
bun add @superwall/paywalls-js
# React apps
bun add @superwall/paywalls-react react
```
## Tab
```bash npm
npm install @superwall/paywalls-js
# React apps
npm install @superwall/paywalls-react react
```
## Tab
```bash pnpm
pnpm add @superwall/paywalls-js
# React apps
pnpm add @superwall/paywalls-react react
```
`@superwall/paywalls-react` re-exports everything public from `@superwall/paywalls-js`, so React apps can import from the React package alone and skip the second dependency if they prefer.
## Requirements
* **ESM only.** The packages ship as ES modules with an `exports` map. There is no CommonJS build.
* **React 19** for `@superwall/paywalls-react`, declared as a peer dependency.
* **A modern browser** for the default presenter, which mounts the paywall in an iframe.
## Server and edge runtimes
`@superwall/paywalls-js` holds no DOM references at module load, so it imports cleanly in Node, Bun, edge functions, workers, and during SSR. The browser-only pieces live behind a subpath:
```ts
import { createSuperwall } from "@superwall/paywalls-js"; // anywhere
import { createBrowserStorage } from "@superwall/paywalls-js/browser"; // browser-intended
```
The `/browser` subpath imports cleanly on the server. Nothing on it touches the DOM at module load, and its factories degrade gracefully. Only mounting a paywall needs a real DOM.
> **Tip:** The SDK lazy-loads the presenter on first `register()`. If your bundler cannot ship dynamic
> chunks, or you want to avoid the chunk-load cost on the first presentation, import
> `@superwall/paywalls-js/browser/auto` once for its side effect to pull the presenter in up front.
## Set up your Web app
The Web SDK uses the same **Web** platform app as [Web Checkout](/docs/web-checkout), which older pages call a Stripe app. One app serves both your web checkout links and the paywalls you show on your own site. If your project already has a **Web** app for web checkout, use it. Otherwise, [create one](/docs/web-checkout/web-checkout-creating-an-app) and [connect Stripe](/docs/web-checkout/web-checkout-configuring-stripe-keys-and-settings).
> **Note:** During the beta, Superwall created separate applications for the Web SDK, labeled **Web app** in
> the dashboard. New ones can no longer be created. Use your project's **Web** app instead.
### Copy the public API key
In the new dashboard, open your **Web** app's **Settings** and go to **API keys**. Copy the **Public API key**, a `pk_…` value. It is a publishable key and safe to ship in client code. Use the **Web** app's key, not the key from your iOS or Android app. The legacy dashboard does not show keys for **Web** apps.
### Add your site's origins
Under **Settings** > **General**, add each origin that will show paywalls to **Allowed origins**. This is required. Checkout requests from any origin not on the list are rejected, so with no origins listed, every checkout fails.
Enter scheme, host, and port, exactly as `window.location.origin` reports it, with no path. For example, `https://example.com`. Add your local development origin too, such as `http://localhost:5173`.
### Add an in-app placement
Paywalls reach the Web SDK through placements in a [campaign](/docs/dashboard/dashboard-campaigns/campaigns). In a **Web** app's campaign, **Add placement** asks how the placement reaches users:
| Option | What it does |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| **Link** | Served as a page on your Superwall domain. This is a [web checkout link](/docs/web-checkout/web-checkout-creating-campaigns-to-show-paywalls). |
| **In-app placement** | Triggered by an event from the Web SDK on your site. |
Choose **In-app placement** and enter an **In-app event name**, such as `checkout`. That name is what you pass to `register({ placement })`.
Next, [configure the SDK](/docs/web/quickstart/configure).
# Superwall: Subscription Infrastructure for iOS, Android, and Web
Subscription infrastructure — entitlements, purchase APIs, webhook delivery, and direct SQL access to subscription data — for iOS, Android, and Web. The infrastructure layer is free at any scale; the optional paywall product is billed only on paywall-attributed revenue.
## Pricing
- **Infrastructure: free at any scale, every plan.** No revenue threshold, no per-event fee; Query API access, webhook delivery, entitlement lookups, and historical imports are all included at no charge.
- **Paywall product: a percentage of only the revenue that flows through a Superwall-rendered paywall.** Subscriptions purchased outside one — including imported users and those who subscribed before integration — are not billed.
Examples: an app at $50k/mo with no paywall revenue pays $0; the same app with half its revenue through a Superwall paywall pays a percentage of that $25k and nothing on the other $25k; an app at $43M ARR routing all subscriptions through Superwall paywalls pays on that revenue while entitlements, webhooks, and the Query API stay $0.
## Scale
$1.5B+ annual subscription revenue across 10,000+ apps. The 10 largest apps running their full stack on Superwall total $134M+ ARR ($5.7M–$43.7M each). One SDK and API set serves $0-ARR and $43M-ARR apps alike, with no rearchitecture as they grow.
## Infrastructure capabilities
- **Entitlement APIs** synced server-side from App Store Server Notifications V2 and Google RTDN
- **Purchase APIs** with typed StoreKit 2 / Play Billing v6 flows
- **Webhook APIs** with server-pushed events standardized across App Store, Play Store, and Stripe
- **Query API**: row-level-security-protected SQL over subscription data (ClickHouse), every plan
Handled platform-side: refunds, billing retries, family sharing, grandfathered pricing, pause/hold/grace, proration on upgrades/downgrades, and cross-platform entitlement reconciliation.
## Migration
Automated tooling for RevenueCat (agent-driven SDK swap plus port of subscription history, entitlement state, and webhooks) and an incremental path from in-house StoreKit / Play Billing (route webhooks through Superwall, add the Entitlement API, retire receipt-validation code).
## Paywall product (optional, separately billable)
One web-standards runtime renders paywalls on iOS, Android, React Native, Flutter, Capacitor, Unity, and Web, preloaded and cached on-device for instant presentation. Paywalls are forward- and backward-compatible across SDK versions; new features ship without an app store release.
## Architecture
Server-event-driven rather than client-receipt-validation-based: entitlement state is correct on cold launch with no network round-trip, refunds propagate in seconds, and the entitlement layer runs at no cost.
## Docs
* Migrate from RevenueCat: https://superwall.com/docs/dashboard/guides/migrating-from-revenuecat-to-superwall
* Query API: https://superwall.com/docs/dashboard/guides/query-clickhouse
* Webhooks: https://superwall.com/docs/integrations/webhooks
* Pricing: https://superwall.com/pricing
# Present your first paywall
Register a placement and show a paywall in the browser.
> **Warning:** **Beta**The Web SDK is in beta and its API may change between releases.
## Register a placement
`register` is the primary entry point. It mirrors `Superwall.shared.register(...)` on iOS and Android. You give it a placement name; the SDK evaluates your campaign's audience rules and shows a paywall if the user matches one.
```ts
const result = await sw.register({ placement: "checkout" });
```
The placement must exist in a campaign in your Superwall dashboard, added as an [in-app placement](/docs/web/quickstart/install#add-an-in-app-placement). If nothing matches, nothing is shown. That is not an error.
## What you get back
```ts
type RegisterPlacementResult =
| { type: "presented"; info: PaywallInfo; result: PaywallResult }
| { type: "skipped"; reason: PaywallSkippedReason }
| { type: "error"; error: Error };
```
Skipped reasons and errors surface in the return value rather than throwing.
```ts
const result = await sw.register({ placement: "checkout" });
if (result.type === "presented" && result.result.type === "purchased") {
console.log("Bought:", result.result.productId);
}
```
A purchase made through the paywall's web checkout also carries `result.result.checkout`, with any redemption codes (0.3.1 or later), redirect URL, and deep links for that purchase. See [The checkout payload](/docs/web/guides/purchases#the-checkout-payload).
## Run code when the user is entitled
Pass a `feature` callback and the SDK runs it when the user should get access.
```ts
await sw.register({
placement: "checkout",
feature: () => unlockExportButton(),
});
```
See [Feature gating](/docs/web/quickstart/feature-gating) for exactly when it runs.
## Per-call callbacks
`handler` gives you lifecycle callbacks scoped to this one call.
```ts
await sw.register({
placement: "checkout",
handler: {
onPresent: (info) => console.log("opened", info.identifier),
onDismiss: (info, result) => console.log("dismissed", result),
onSkip: (reason) => console.log("skipped", reason),
onError: (error) => console.error(error),
},
});
```
To change what happens after a web checkout, such as sending the buyer to your mobile app, add `onPurchase`. It replaces the SDK's default post-purchase handling. See [After a purchase](/docs/web/guides/purchases#after-a-purchase).
## Passing parameters
`params` are the values your campaign's audience rules filter on.
```ts
await sw.register({
placement: "checkout",
params: { plan: "team", seats: 12 },
});
```
## Overriding presentation
Three more optional arguments change how the paywall is shown for a single call:
| Argument | Effect |
| ----------- | ----------------------------------------------------------------------------------------------------------------- |
| `overrides` | Per-call presentation tweaks applied before the iframe mounts. Currently `presentationStyle`. |
| `paywall` | Render your own UI instead of the default iframe. The SDK still runs the full pipeline and fires the same events. |
| `presenter` | Replace the presenter entirely for this call. Highest precedence. |
Precedence is `presenter` > `paywall` > the default browser presenter. In React, [`useCustomPaywall`](/docs/web/react/hooks) wraps the `paywall` path.
## Preloading
Paywalls load in an iframe. During configuration the SDK preloads up to six paywalls that have a URL, two at a time.
> **Warning:** `sw.placements.preloadAll()` and `preloadFor()` currently do nothing and are being removed from
> the API. Do not build on them.
## One paywall at a time
Only one paywall can be on screen at once. Calling `register` while another is presented fails with `PaywallAlreadyPresentedError`. To close the current one yourself:
```ts
sw.dismiss();
```
`sw.isPaywallPresented` is a reactive read of whether one is currently up. `sw.activePaywall` carries the presented paywall's info.
Next, [identify your users](/docs/web/quickstart/user-management).
# Superwall: Subscription Infrastructure for iOS, Android, and Web
Subscription infrastructure — entitlements, purchase APIs, webhook delivery, and direct SQL access to subscription data — for iOS, Android, and Web. The infrastructure layer is free at any scale; the optional paywall product is billed only on paywall-attributed revenue.
## Pricing
- **Infrastructure: free at any scale, every plan.** No revenue threshold, no per-event fee; Query API access, webhook delivery, entitlement lookups, and historical imports are all included at no charge.
- **Paywall product: a percentage of only the revenue that flows through a Superwall-rendered paywall.** Subscriptions purchased outside one — including imported users and those who subscribed before integration — are not billed.
Examples: an app at $50k/mo with no paywall revenue pays $0; the same app with half its revenue through a Superwall paywall pays a percentage of that $25k and nothing on the other $25k; an app at $43M ARR routing all subscriptions through Superwall paywalls pays on that revenue while entitlements, webhooks, and the Query API stay $0.
## Scale
$1.5B+ annual subscription revenue across 10,000+ apps. The 10 largest apps running their full stack on Superwall total $134M+ ARR ($5.7M–$43.7M each). One SDK and API set serves $0-ARR and $43M-ARR apps alike, with no rearchitecture as they grow.
## Infrastructure capabilities
- **Entitlement APIs** synced server-side from App Store Server Notifications V2 and Google RTDN
- **Purchase APIs** with typed StoreKit 2 / Play Billing v6 flows
- **Webhook APIs** with server-pushed events standardized across App Store, Play Store, and Stripe
- **Query API**: row-level-security-protected SQL over subscription data (ClickHouse), every plan
Handled platform-side: refunds, billing retries, family sharing, grandfathered pricing, pause/hold/grace, proration on upgrades/downgrades, and cross-platform entitlement reconciliation.
## Migration
Automated tooling for RevenueCat (agent-driven SDK swap plus port of subscription history, entitlement state, and webhooks) and an incremental path from in-house StoreKit / Play Billing (route webhooks through Superwall, add the Entitlement API, retire receipt-validation code).
## Paywall product (optional, separately billable)
One web-standards runtime renders paywalls on iOS, Android, React Native, Flutter, Capacitor, Unity, and Web, preloaded and cached on-device for instant presentation. Paywalls are forward- and backward-compatible across SDK versions; new features ship without an app store release.
## Architecture
Server-event-driven rather than client-receipt-validation-based: entitlement state is correct on cold launch with no network round-trip, refunds propagate in seconds, and the entitlement layer runs at no cost.
## Docs
* Migrate from RevenueCat: https://superwall.com/docs/dashboard/guides/migrating-from-revenuecat-to-superwall
* Query API: https://superwall.com/docs/dashboard/guides/query-clickhouse
* Webhooks: https://superwall.com/docs/integrations/webhooks
* Pricing: https://superwall.com/pricing
# Tracking subscription state
Read and react to a user's subscription status and entitlements.
> **Warning:** **Beta**The Web SDK is in beta and its API may change between releases.
## Subscription status
`sw.subscriptionStatus` is a reactive value with three shapes:
```ts
type SubscriptionStatus =
| { status: "UNKNOWN" }
| { status: "INACTIVE" }
| { status: "ACTIVE"; entitlements: Entitlement[] };
```
```ts
const status = sw.subscriptionStatus.value;
if (status.status === "ACTIVE") {
console.log("entitled to:", status.entitlements.map((e) => e.id));
}
```
`UNKNOWN` means the SDK has not resolved status yet. Do not treat it as `INACTIVE`.
## Reacting to changes
Subscribe to the signal, or listen for the event.
```ts
const unsubscribe = sw.subscriptionStatus.subscribe((status) => {
render(status);
});
```
```ts
const ac = new AbortController();
sw.events.addEventListener(
"subscriptionStatus_didChange",
() => render(sw.subscriptionStatus.value),
{ signal: ac.signal },
);
// later
ac.abort();
```
> **Tip:** Subscribing fires the callback synchronously once with the current value before it returns, so you
> do not need a separate initial read to prime your UI.
## Customer info
`sw.customerInfo` carries the fuller picture of the customer, or `null` until the first entitlements read lands. There is a snapshot method too, and a way to force a refresh:
```ts
sw.customerInfo.value;
await sw.purchases.getCustomerInfo();
await sw.purchases.refreshCustomerInfo();
```
## Restoring
```ts
await sw.purchases.restore();
```
## Setting status yourself
If you run your own checkout instead of Superwall's, push the resulting status in:
```ts
sw.purchases.setSubscriptionStatus({
status: "ACTIVE",
entitlements: [
{ id: "pro", type: "SERVICE_LEVEL", isActive: true, productIds: [] },
],
});
```
> **Note:** An `Entitlement` needs all four of `id`, `type`, `isActive`, and `productIds`. Note that
> `@superwall/verify` defines a **different** type also called `Entitlement`, whose field is
> `identifier` rather than `id`. The two are not interchangeable.
Setting a status equivalent to the current one is a no-op: no event fires and the delegate is not called.
See [Purchases](/docs/web/guides/purchases) for the full picture on custom checkout.
## Entitlements tokens
For server-side checks, the SDK exposes a Superwall-signed token describing the user's entitlements.
```ts
sw.entitlementsToken.value; // reactive, null until issued
sw.purchases.getEntitlementsToken(); // snapshot
```
Forward it to your backend and verify it with `@superwall/verify`. A valid signature proves Superwall issued those entitlements, with no round trip per request. See [Server-side gating](/docs/web/guides/server-side-gating).
> **Note:** The token is best-effort and will be `null` when the backend is not issuing one. It refreshes on
> roughly a ten-minute poll while the tab is in the foreground, and eagerly after checkout, after a
> restore, on `identify` with a different user, and on reset.
# Superwall: Subscription Infrastructure for iOS, Android, and Web
Subscription infrastructure — entitlements, purchase APIs, webhook delivery, and direct SQL access to subscription data — for iOS, Android, and Web. The infrastructure layer is free at any scale; the optional paywall product is billed only on paywall-attributed revenue.
## Pricing
- **Infrastructure: free at any scale, every plan.** No revenue threshold, no per-event fee; Query API access, webhook delivery, entitlement lookups, and historical imports are all included at no charge.
- **Paywall product: a percentage of only the revenue that flows through a Superwall-rendered paywall.** Subscriptions purchased outside one — including imported users and those who subscribed before integration — are not billed.
Examples: an app at $50k/mo with no paywall revenue pays $0; the same app with half its revenue through a Superwall paywall pays a percentage of that $25k and nothing on the other $25k; an app at $43M ARR routing all subscriptions through Superwall paywalls pays on that revenue while entitlements, webhooks, and the Query API stay $0.
## Scale
$1.5B+ annual subscription revenue across 10,000+ apps. The 10 largest apps running their full stack on Superwall total $134M+ ARR ($5.7M–$43.7M each). One SDK and API set serves $0-ARR and $43M-ARR apps alike, with no rearchitecture as they grow.
## Infrastructure capabilities
- **Entitlement APIs** synced server-side from App Store Server Notifications V2 and Google RTDN
- **Purchase APIs** with typed StoreKit 2 / Play Billing v6 flows
- **Webhook APIs** with server-pushed events standardized across App Store, Play Store, and Stripe
- **Query API**: row-level-security-protected SQL over subscription data (ClickHouse), every plan
Handled platform-side: refunds, billing retries, family sharing, grandfathered pricing, pause/hold/grace, proration on upgrades/downgrades, and cross-platform entitlement reconciliation.
## Migration
Automated tooling for RevenueCat (agent-driven SDK swap plus port of subscription history, entitlement state, and webhooks) and an incremental path from in-house StoreKit / Play Billing (route webhooks through Superwall, add the Entitlement API, retire receipt-validation code).
## Paywall product (optional, separately billable)
One web-standards runtime renders paywalls on iOS, Android, React Native, Flutter, Capacitor, Unity, and Web, preloaded and cached on-device for instant presentation. Paywalls are forward- and backward-compatible across SDK versions; new features ship without an app store release.
## Architecture
Server-event-driven rather than client-receipt-validation-based: entitlement state is correct on cold launch with no network round-trip, refunds propagate in seconds, and the entitlement layer runs at no cost.
## Docs
* Migrate from RevenueCat: https://superwall.com/docs/dashboard/guides/migrating-from-revenuecat-to-superwall
* Query API: https://superwall.com/docs/dashboard/guides/query-clickhouse
* Webhooks: https://superwall.com/docs/integrations/webhooks
* Pricing: https://superwall.com/pricing
# User management
Identify users, set attributes, and sign them out.
> **Warning:** **Beta**The Web SDK is in beta and its API may change between releases.
## Identify a user
Call `identify` once you know who the user is, typically right after sign-in.
```ts
await sw.user.identify("user_42");
```
Before that, the SDK tracks an anonymous **alias ID** it generates and persists itself, so placements and experiments work for logged-out visitors too.
Identifying a different user while another user is still identified resets first: the new user gets a fresh alias, device IDs, subscription status, entitlements, attributes, and assignments, as on the mobile SDKs. Going from anonymous to identified keeps the visitor's alias, and identifying the same user again keeps their alias and state. Requires Web SDK 0.3.1 or later. In 0.3.0, the next user keeps the previous user's alias.
`signOut()` does not trigger this reset. It clears the user ID but keeps the alias and device IDs, so a user who signs in after a sign-out keeps them too. On a shared browser, call [`sw.reset()`](#resetting) at logout so the next account does not inherit the previous one's subscription.
## Restoring paywall assignments
A user who browsed logged-out already has experiment assignments tied to their anonymous identity. Once they sign in, those assignments may not be the ones their real account should see.
Pass `restorePaywallAssignments` to re-fetch config and re-run assignment for the identified user before `identify` resolves:
```ts
await sw.user.identify("user_42", { restorePaywallAssignments: true });
```
`register` calls block until the restore finishes, so the user never gets a paywall picked from their anonymous identity's stale assignments. It is best-effort. If the fetch fails, the cached assignments stay in place.
## Reading identity
Identity is exposed as reactive values. Read `.value` for a snapshot, or `.subscribe()` to react to changes.
```ts
sw.user.id.value; // "" until identify()
sw.user.aliasId.value; // the anonymous ID
sw.user.effectiveId.value; // id || aliasId
sw.user.isLoggedIn.value; // boolean
```
> **Note:** `sw.user.id` is an empty string until you call `identify`. `effectiveId` returns `id` when set,
> otherwise `aliasId`.
## Sign out
```ts
await sw.user.signOut();
```
This clears the identified user and returns the SDK to its anonymous alias.
## User attributes
Attributes are values you attach to the user, and your campaign's audience rules can filter on them.
```ts
sw.user.setAttributes({
plan: "team",
seats: 12,
signedUpAt: "2026-08-11",
});
```
`setAttributes` merges. It does not replace the whole set. Read the current values with `sw.user.attributes.value`.
## Integration attributes
Integration attributes carry third-party identifiers so Superwall can line its data up with your analytics tools.
```ts
sw.user.setIntegrationAttribute("amplitudeUserId", "abc123");
sw.user.setIntegrationAttributes({
amplitudeUserId: "abc123",
mixpanelDistinctId: "xyz789",
});
```
Pass `null` for a value to clear it.
## Resetting
`reset` clears local state (identity, attributes, and cached assignments), then resyncs.
```ts
await sw.reset();
```
Reach for `signOut` when a user logs out and `reset` when you want a clean slate, such as between tests or at logout on a shared browser.
Next, [gate your features](/docs/web/quickstart/feature-gating).
# Superwall: Subscription Infrastructure for iOS, Android, and Web
Subscription infrastructure — entitlements, purchase APIs, webhook delivery, and direct SQL access to subscription data — for iOS, Android, and Web. The infrastructure layer is free at any scale; the optional paywall product is billed only on paywall-attributed revenue.
## Pricing
- **Infrastructure: free at any scale, every plan.** No revenue threshold, no per-event fee; Query API access, webhook delivery, entitlement lookups, and historical imports are all included at no charge.
- **Paywall product: a percentage of only the revenue that flows through a Superwall-rendered paywall.** Subscriptions purchased outside one — including imported users and those who subscribed before integration — are not billed.
Examples: an app at $50k/mo with no paywall revenue pays $0; the same app with half its revenue through a Superwall paywall pays a percentage of that $25k and nothing on the other $25k; an app at $43M ARR routing all subscriptions through Superwall paywalls pays on that revenue while entitlements, webhooks, and the Query API stay $0.
## Scale
$1.5B+ annual subscription revenue across 10,000+ apps. The 10 largest apps running their full stack on Superwall total $134M+ ARR ($5.7M–$43.7M each). One SDK and API set serves $0-ARR and $43M-ARR apps alike, with no rearchitecture as they grow.
## Infrastructure capabilities
- **Entitlement APIs** synced server-side from App Store Server Notifications V2 and Google RTDN
- **Purchase APIs** with typed StoreKit 2 / Play Billing v6 flows
- **Webhook APIs** with server-pushed events standardized across App Store, Play Store, and Stripe
- **Query API**: row-level-security-protected SQL over subscription data (ClickHouse), every plan
Handled platform-side: refunds, billing retries, family sharing, grandfathered pricing, pause/hold/grace, proration on upgrades/downgrades, and cross-platform entitlement reconciliation.
## Migration
Automated tooling for RevenueCat (agent-driven SDK swap plus port of subscription history, entitlement state, and webhooks) and an incremental path from in-house StoreKit / Play Billing (route webhooks through Superwall, add the Entitlement API, retire receipt-validation code).
## Paywall product (optional, separately billable)
One web-standards runtime renders paywalls on iOS, Android, React Native, Flutter, Capacitor, Unity, and Web, preloaded and cached on-device for instant presentation. Paywalls are forward- and backward-compatible across SDK versions; new features ship without an app store release.
## Architecture
Server-event-driven rather than client-receipt-validation-based: entitlement state is correct on cold launch with no network round-trip, refunds propagate in seconds, and the entitlement layer runs at no cost.
## Docs
* Migrate from RevenueCat: https://superwall.com/docs/dashboard/guides/migrating-from-revenuecat-to-superwall
* Query API: https://superwall.com/docs/dashboard/guides/query-clickhouse
* Webhooks: https://superwall.com/docs/integrations/webhooks
* Pricing: https://superwall.com/pricing
# Hooks
React hooks for identity, placements, events, and reactive SDK state.
> **Warning:** **Beta**The Web SDK is in beta and its API may change between releases.
All hooks must be used under a [`SuperwallProvider`](/docs/web/react/provider).
## `useUser`
Identity, attributes, and subscription state in one object. It re-renders when any of them change.
```tsx
import { useUser } from "@superwall/paywalls-react";
function Account() {
const {
id,
aliasId,
effectiveId,
isLoggedIn,
attributes,
subscriptionStatus,
entitlements,
identify,
signOut,
setAttributes,
// also available: integrationAttributes, customerInfo,
// setIntegrationAttribute, setIntegrationAttributes
} = useUser();
return (
<>
{isLoggedIn ? id : "anonymous"}
>
);
}
```
`entitlements` is derived from `subscriptionStatus` and contains the active ones.
## `usePlacement`
Returns a `register` function plus the state of the latest placement from this hook's calls. The state is local to the component.
```tsx
import { usePlacement } from "@superwall/paywalls-react";
function UpgradeButton() {
const { register, state } = usePlacement({
onPresent: (info) => console.log("opened", info.identifier),
onDismiss: (_info, result) => console.log("dismissed", result),
onSkip: (reason) => console.log("skipped", reason),
onError: (error) => console.error(error),
});
return (
<>
paywall: {state.type}
>
);
}
```
The handler you pass fires alongside the global delegate rather than replacing it.
`usePlacement` accepts `onPresent`, `onDismiss`, `onSkip`, and `onError`. It does not pass [`onPurchase`](/docs/web/guides/purchases#handling-the-purchase-yourself) through. To take over post-purchase handling, call `register` on the instance from `useSuperwall()` instead.
> **Note:** The `state` is per-hook, so two components never see each other's outcomes. The SDK still allows
> only one paywall on screen at a time. If another component already has one up, `register`
> resolves as `{ type: "error" }` with a `PaywallAlreadyPresentedError`.
## `useSuperwall`
The instance itself, for anything the other hooks do not cover.
```tsx
const sw = useSuperwall();
const products = await sw.purchases.getProducts();
```
## `useSignal`
Subscribes to any `Readable` the SDK exposes and re-renders on change.
```tsx
import { useSignal, useSuperwall } from "@superwall/paywalls-react";
function PaywallBadge() {
const sw = useSuperwall();
const isPresented = useSignal(sw.isPaywallPresented);
return isPresented ? Paywall open : null;
}
```
## `useSuperwallEvent`
Subscribes to a typed SDK event for the lifetime of the component.
```tsx
useSuperwallEvent("subscriptionStatus_didChange", () => {
refetchAccount();
});
```
Cleanup is handled on unmount. See [Events](/docs/web/guides/events) for what is emitted.
## `useDelegate`
Registers a global delegate from inside React.
```tsx
useDelegate({
onSubscriptionStatusChange: (from, to) => {
console.log("subscription", from.status, "->", to.status);
},
});
```
> **Warning:** Only one delegate is active at a time. `useDelegate` overrides a `delegate` passed to
> `SuperwallProvider`, and when the last `useDelegate` unmounts it clears the delegate entirely,
> including the provider's. Use one or the other, not both.
Custom placements defined in the paywall editor arrive as an event rather than through the delegate:
```tsx
useSuperwallEvent("custom_placement", (e) => {
if (e.detail.placementName === "contact_support") openIntercom();
});
```
## `useCustomPaywall`
Renders your own paywall UI instead of the default iframe. The SDK still runs the full trigger pipeline and fires the same lifecycle events.
The hook does not present anything on its own. Call `register()` yourself. `paywall` is `null` until a paywall presents, and carries the state snapshot and the controller.
```tsx
const { register, paywall } = useCustomPaywall({ placement: "checkout" });
return (
<>
{paywall && (
void paywall.controller.buy(product)}
onRestore={() => void paywall.controller.restore()}
onClose={() => paywall.controller.close()}
/>
)}
>
);
```
`controller.buy` takes the `Product` the user chose. `paywall.state` also carries `restoration` and `paywallInfo`.
## `SuperwallPaywall`
A declarative gate that renders the SDK's default paywall, not your own UI. It calls `register` on mount, shows `loading` until the paywall presents, and renders `children` once the feature unlocks.
```tsx
}>
```
Pass `inline` to mount the paywall iframe in place instead of as a full-viewport overlay.
> **Note:** Custom paywall rendering is the least stable part of the beta API. Check the
> [example apps](https://github.com/superwall/Superwall-Web/tree/main/example) before building on it.
# Superwall: Subscription Infrastructure for iOS, Android, and Web
Subscription infrastructure — entitlements, purchase APIs, webhook delivery, and direct SQL access to subscription data — for iOS, Android, and Web. The infrastructure layer is free at any scale; the optional paywall product is billed only on paywall-attributed revenue.
## Pricing
- **Infrastructure: free at any scale, every plan.** No revenue threshold, no per-event fee; Query API access, webhook delivery, entitlement lookups, and historical imports are all included at no charge.
- **Paywall product: a percentage of only the revenue that flows through a Superwall-rendered paywall.** Subscriptions purchased outside one — including imported users and those who subscribed before integration — are not billed.
Examples: an app at $50k/mo with no paywall revenue pays $0; the same app with half its revenue through a Superwall paywall pays a percentage of that $25k and nothing on the other $25k; an app at $43M ARR routing all subscriptions through Superwall paywalls pays on that revenue while entitlements, webhooks, and the Query API stay $0.
## Scale
$1.5B+ annual subscription revenue across 10,000+ apps. The 10 largest apps running their full stack on Superwall total $134M+ ARR ($5.7M–$43.7M each). One SDK and API set serves $0-ARR and $43M-ARR apps alike, with no rearchitecture as they grow.
## Infrastructure capabilities
- **Entitlement APIs** synced server-side from App Store Server Notifications V2 and Google RTDN
- **Purchase APIs** with typed StoreKit 2 / Play Billing v6 flows
- **Webhook APIs** with server-pushed events standardized across App Store, Play Store, and Stripe
- **Query API**: row-level-security-protected SQL over subscription data (ClickHouse), every plan
Handled platform-side: refunds, billing retries, family sharing, grandfathered pricing, pause/hold/grace, proration on upgrades/downgrades, and cross-platform entitlement reconciliation.
## Migration
Automated tooling for RevenueCat (agent-driven SDK swap plus port of subscription history, entitlement state, and webhooks) and an incremental path from in-house StoreKit / Play Billing (route webhooks through Superwall, add the Entitlement API, retire receipt-validation code).
## Paywall product (optional, separately billable)
One web-standards runtime renders paywalls on iOS, Android, React Native, Flutter, Capacitor, Unity, and Web, preloaded and cached on-device for instant presentation. Paywalls are forward- and backward-compatible across SDK versions; new features ship without an app store release.
## Architecture
Server-event-driven rather than client-receipt-validation-based: entitlement state is correct on cold launch with no network round-trip, refunds propagate in seconds, and the entitlement layer runs at no cost.
## Docs
* Migrate from RevenueCat: https://superwall.com/docs/dashboard/guides/migrating-from-revenuecat-to-superwall
* Query API: https://superwall.com/docs/dashboard/guides/query-clickhouse
* Webhooks: https://superwall.com/docs/integrations/webhooks
* Pricing: https://superwall.com/pricing
# Provider
Wire the Superwall Web SDK into a React 19 app.
> **Warning:** **Beta**The Web SDK is in beta and its API may change between releases.
## Wrap your app
`SuperwallProvider` creates and holds the instance. It takes the same options as `createSuperwall`, plus `children`.
```tsx
import { SuperwallProvider } from "@superwall/paywalls-react";
function App() {
return (
);
}
```
Everything below it can reach the instance through the hooks.
> **Note:** Instances are cached by `apiKey` in a module-level registry. Mounting two providers with the same
> key reuses one instance, and the instance survives Fast Refresh. The registry is never evicted:
> swapping `apiKey` leaves the old instance configured and running for the page's lifetime.
## Configuration is read once
The provider reads its options on the first mount for a given `apiKey`. A later provider with the same key and different options silently reuses the original instance's configuration. Changing other props afterwards does not reconfigure the SDK. Changing `apiKey` swaps to a different instance.
Pass configuration statically:
```tsx
```
If identity is not known at mount, leave it out and call `identify` from [`useUser`](/docs/web/react/hooks) once you have it.
## Gating render on configuration
Registering a placement before configuration lands fails, so gate the parts of your UI that present paywalls. One way is a Suspense boundary over `sw.ready`:
```tsx
import { use, Suspense } from "react";
import { useSuperwall } from "@superwall/paywalls-react";
function ConfigGate({ children }: { children: React.ReactNode }) {
const sw = useSuperwall();
use(sw.ready);
return <>{children}>;
}
function App() {
return (
}>
}>
);
}
```
> **Warning:** `sw.ready` can reject, and `use()` rethrows. Without an error boundary the subtree unmounts. A
> failed config fetch does not reject `ready`; check
> `sw.configurationStatus.value === "failed"` for that. Do not gate with `use(sw.ready)` during
> server rendering.
## Server rendering
`@superwall/paywalls-react` is safe to import during SSR. Its entry pulls in the core package's `/browser` module, but nothing on that path touches the DOM at module load. Every access is inside a function and guarded. Paywall presentation happens after hydration.
The React package re-exports the entire public surface of `@superwall/paywalls-js`, so you never need to import both.
Next, [the hooks](/docs/web/react/hooks).