# 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 Reference

Every hook the framework provides, signatures, what each returns, and the semantics that matter.

Everything a paywall reads or triggers comes through hooks. One concern each. There is deliberately no kitchen-sink hook.

```tsx
import {
  useProducts, usePurchase, useDiscount, useActions, useCheckoutRedemption, useHaptics,
  useTranslation, useIntroductoryOffer, useDevice, useUser, useVariables, useColorScheme,
  useSuperwallEvent, useSuperwallSnapshot, useSuperwallSession,
  type ProductReference,
} from "superwall/hooks";

import { useRouter, useIsFocused } from "superwall/navigation";
```

## `useProducts()`

```tsx
const { products, getProduct } = useProducts();
const annual = getProduct("annual");   // typed reference — typos are compile errors
```

Products, keyed by the reference declared in `config.ts`, each carrying store-owned `variables` (`price`, `period`, `trialPeriodDays`, …). A declared reference always exists, but its variables may not have arrived, guard every read and design the empty state. The full variable list and reading rules are in [Products](/docs/framework/products).

## `usePurchase()`

```tsx
const { purchase, prefetch, isPurchasing, transaction, failure } = usePurchase();

const result = await purchase("annual");
// { status: "completed" | "abandoned" | "failed" } — never throws for flow outcomes
```

The whole purchase flow (outcomes, the no-loading-state rule, web checkout, and `prefetch`) is in [Purchases](/docs/framework/purchases).

## `useDiscount(options?)`

```tsx
const { redeem, clear, applied, isRedeeming, getDiscountedProduct } = useDiscount({
  onDropped: (code) => toast(`${code} is no longer valid`),
});

const result = await redeem(code);            // { valid: true, … } | { valid: false, reason }
const annual = getDiscountedProduct("annual"); // discounted prices, or undefined
```

Stripe promotion codes for web checkout. A redeemed code rides on every checkout session for the products it applies to. `applied.discount` is Stripe's coupon under Stripe's own field names (`percentOff`, `amountOff`, `duration`, `durationInMonths`), and each product carries only what had to be computed: `amount`, `discountedAmount` and `savings` in minor units, plus `price`, `discountedPrice` and `savingsPrice` formatted for the locale. Only meaningful with `checkout: "sheet"` or `"applePay"`. [Web checkout](/docs/framework/web-checkout) has the flow.

## `useActions()`

```tsx
const { close, restore, openUrl, requestPermission, requestCallback } = useActions();
```

Also on the object: `openExternalUrl`, `openDeepLink`, `customPlacement`, `requestStoreReview`, `setUserAttributes`. Everything a paywall asks its host to do. [Actions](/docs/framework/actions) has the full table, the permission types, and the callback pattern.

## `useCheckoutRedemption()`

```tsx
const { completed, redemptionUrl, appLink, redeem, redirect, finish, openApp } = useCheckoutRedemption();
```

Finishes a web checkout the paywall chose to keep with [`postPurchase: "stay"`](/docs/framework/config#post-purchase). `completed` is false until such a purchase lands; then render your own success screen and call `redeem()` when the shopper taps continue, `openApp()` to jump straight into the installed app through `appLink` (show that button only when `appLink` is set), `redirect(url)` to send them somewhere of yours, or `finish()` to do whatever the paywall's stored behavior says. On any other `postPurchase` the host finishes the purchase itself and `completed` stays false. Only meaningful with web checkout.

## `useHaptics()`

```tsx
const haptics = useHaptics();
haptics.light();      // navigation, CTAs
haptics.selection();  // changing a choice
haptics.success();    // purchase landed
```

Also available: `medium`, `heavy`, `warning`, `error`. Fire one on every meaningful tap, iOS produces no feedback of its own inside a paywall. No-ops where haptics are unavailable, so call them unconditionally.

## `useTranslation()`

```tsx
const { t, locale, setLocale, locales } = useTranslation();
t("paywall.cta", { price });
```

Localized copy from `messages/<locale>.ts` catalogs, the catalog system, fallback rules, and interpolation are in [Localization](/docs/framework/localization).

## `useIntroductoryOffer()`

```tsx
const { eligible } = useIntroductoryOffer();   // boolean | undefined — the store decides
```

Introductory-offer eligibility, the same signal the visual editor calls `hasIntroductoryOffer`. `undefined` until the SDK reports, so gate trial-only UI on `eligible === true`. Splits the paywall into eligible and ineligible versions, both must read as intentional. See [Free trials](/docs/framework/trials).

## `useVariables()`

```tsx
const { device, user, params } = useVariables();
```

Everything the app and SDK told this paywall about the presentation: the SDK-filled `device` record, `user` attributes your app set, and the placement's `params`. All three are host-filled, so guard every read. The records, fields, and guarding doctrine are in [Variables & personalization](/docs/framework/variables).

## `useUser()`

```tsx
const user = useUser();
```

Shorthand for `useVariables().user` when the device and params records aren't needed.

## `useDevice()`

```tsx
const { orientation, platform, deviceModel } = useDevice();
```

The same device record as `useVariables().device`, plus `orientation` (`"portrait" | "landscape"`), measured in the page, so it updates the moment the device turns. See [Variables & personalization](/docs/framework/variables).

## `useLocalResource(id, fallback)`

```tsx
import hero from "@/assets/hero.mp4";

const src = useLocalResource("hero-video", hero);
<video src={src} autoPlay muted loop playsInline />
```

The URL to load for a resource the host app bundled: `swlocal://hero-video` when the app registered that id (`SuperwallOptions.localResources` on iOS), otherwise `fallback`. The same code works on the web, in the studio, and on SDKs without local resources. `useLocalResources()` is the underlying record: `{ ids, has(id), resolve(id, fallback) }`. See [Assets](/docs/framework/assets#local-resources).

## `useColorScheme()`

```tsx
const scheme = useColorScheme();   // "light" | "dark"
```

Rarely needed: the framework already keeps a `dark`/`light` class on `<html>` from what the device reports, so style with plain CSS (`:root.dark { … }`). Reach for the hook only when you need the scheme in JavaScript. Never use `@media (prefers-color-scheme: dark)` as the mechanism, see [Styling](/docs/framework/styling).

## `useSuperwallEvent(name, handler)`

```tsx
useSuperwallEvent("transaction_complete", () => haptics.success());
```

Typed SDK events, subscribed for the component's lifetime; an inline arrow handler is fine. The event list and when each fires: [Lifecycle & events](/docs/framework/lifecycle). For anything a dedicated hook covers (products, trial, variables), use the hook. It cannot miss data that arrived before your component subscribed.

## `useSuperwallSnapshot()`

```tsx
const snapshot = useSuperwallSnapshot();
const opened = snapshot.paywall !== undefined;
```

The whole runtime state as one subscribed object. Its most common use is gating entry animations on presentation, paywalls are preloaded hidden, and `snapshot.paywall` flips when the paywall is actually shown ([Lifecycle & events](/docs/framework/lifecycle)). It also carries `experiment` (the A/B assignment), `locale`, and the current purchase and transaction state.

## `useSuperwallSession()`

```tsx
const session = useSuperwallSession();
session.postMessage({ event_name: "custom", data: "…" });
```

The full session for advanced work: the few methods no hook surfaces (raw protocol messaging, `accept`) and use outside React components. If you're reaching for it for products, purchases, actions, user attributes, or events, use the dedicated hook instead.

## `useRouter()`

```tsx
import { useRouter } from "superwall/navigation";

const router = useRouter();
router.push("plans");
router.replace("terms");
router.back();
router.canGoBack();
router.dismiss(2);
router.dismissAll();
router.dismissTo("goals");
router.name;    // current page
router.depth;   // pages underneath (index = 0)
```

The stack router for multi-page flows, expo-router's API, method for method. Page names autocomplete and reject typos via the generated `superwall.d.ts`. [Pages & navigation](/docs/framework/navigation) covers the stack model, state between pages, and shared chrome.

## `useIsFocused()`

```tsx
import { useIsFocused } from "superwall/navigation";

const focused = useIsFocused();
```

Whether this page is on top of the stack. Pages you navigate away from stay alive. A covered page can't be clicked or focused, and `useIsFocused()` tells it so, so it can pause video or timers. See [Pages & navigation](/docs/framework/navigation).

## `useQueryState(key, parser?)`

```tsx
import { parseAsStringEnum, useQueryState } from "superwall/navigation";

const [goal, setGoal] = useQueryState("goal", parseAsStringEnum(["focus", "habit"]));
const [name, setName] = useQueryState("name");   // string | null
setGoal("focus");                                 // ?goal=focus
setGoal(null);                                    // key removed
```

`useState` whose value lives in the page URL, so a flow resumes from any link: after a reload, in the OS browser after an in-app one, or back from hosted checkout. On a surface with web checkout the URL is kept automatically, and **every answer in a web funnel goes through this hook** (single choice, multi choice, inputs, the selected plan), never `useState`; see [Web Funnels](/docs/framework/web-funnels). On a native host the same hook is plain state shared across pages. The API is nuqs's: `parseAsString`, `parseAsInteger`, `parseAsFloat`, `parseAsBoolean`, `parseAsStringEnum`, `parseAsArrayOf`, `createParser`, plus `.withDefault()` and `.withOptions({ history, clearOnDefault })`. See [Pages & navigation](/docs/framework/navigation).

## `ProductReference`

```tsx
import { type ProductReference } from "superwall/hooks";

const [selected, setSelected] = React.useState<ProductReference>("annual");
```

The union of product references declared in your `config.ts`, the type behind `getProduct`, `purchase`, and `prefetch`. Use it for selection state so an invalid reference is a compile error.