# 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. 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);
}
```

## 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),
  },
});
```

## 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).