# 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 present a Superwall paywall from shared code.

> **Warning:** **Beta**The KMP SDK is in beta and its API may change between releases.

## Register a placement

Paywalls are presented by registering a **placement**. You do not tell the SDK to show a paywall. You tell it a placement occurred, and your campaign on the dashboard decides what happens.

```kotlin
Superwall.register(placement = "campaign_trigger")
```

That one line is a complete integration. Whether a paywall appears, which one, and to whom, is all configured on the dashboard without shipping an app update.

> **Note:** The placement name must match one you have added to a campaign on the
> [dashboard](https://superwall.com/dashboard). A name that is not in any campaign resolves to
> `PaywallSkippedReason.PlacementNotFound`.

## Gate a feature behind it

Pass a `feature` closure to run code that should only happen if the user is entitled to it:

```kotlin
Superwall.register(placement = "campaign_trigger") {
    launchTheFeature()
}
```

When the closure runs depends on the paywall's feature-gating behavior: immediately when no paywall shows, or after a purchase or restore when the placement is gated. [Feature gating](/docs/kmp/quickstart/feature-gating) covers the rules.

## Pass parameters

`params` are usable in audience filters and on the paywall itself:

```kotlin
Superwall.register(
    placement = "campaign_trigger",
    params = mapOf(
        "source" to "onboarding",
        "isTrialEligible" to true,
    ),
)
```

Values may be `String`, `Boolean`, `Long`, `Double`, `List`, `Map`, or `Set`. Anything else is stringified.

## Observe what happened

A `PaywallPresentationHandler` reports on the presentation. Set only the closures you care about; unset ones are never invoked.

```kotlin
import com.superwall.sdk.kmp.PaywallPresentationHandler
import com.superwall.sdk.kmp.models.results.PaywallResult
import com.superwall.sdk.kmp.models.results.PaywallSkippedReason

val handler = PaywallPresentationHandler()

handler.onPresent { info ->
    println("Presented ${info.name}")
}

handler.onDismiss { info, result ->
    when (result) {
        is PaywallResult.Purchased -> println("Purchased ${result.productId}")
        is PaywallResult.Restored -> println("Restored")
        is PaywallResult.Declined -> println("Declined")
    }
}

handler.onSkip { reason ->
    when (reason) {
        is PaywallSkippedReason.Holdout -> println("Holdout: ${reason.experiment.id}")
        is PaywallSkippedReason.NoAudienceMatch -> println("No audience match")
        is PaywallSkippedReason.PlacementNotFound -> println("Placement not found")
    }
}

handler.onError { error ->
    println("Paywall error: $error")
}

Superwall.register(
    placement = "campaign_trigger",
    handler = handler,
) {
    launchTheFeature()
}
```

> **Note:** `onSkip` is not a failure path. A holdout or an unmatched audience filter means your campaign
> worked as configured. The user was not meant to see a paywall.

All handler closures and the `feature` closure are delivered on the **main thread**, on both platforms. You can touch UI from them directly.

### Custom callbacks

`onCustomCallback` is the one closure that returns a value. A paywall can request an action from your app and branch on the result:

```kotlin
import com.superwall.sdk.kmp.models.callbacks.CustomCallbackResult

handler.onCustomCallback { callback ->
    when (callback.name) {
        "validate_email" -> {
            val email = callback.variables?.get("email") as? String
            if (isValidEmail(email)) {
                CustomCallbackResult.success(mapOf("validated" to true))
            } else {
                CustomCallbackResult.failure(mapOf("error" to "Invalid email"))
            }
        }
        else -> CustomCallbackResult.failure()
    }
}
```

It is a `suspend` closure, so you can do real work in it. When it is unset, the SDK responds with `CustomCallbackResult.failure()`.

## Check before you register

`getPresentationResult` previews what registering *would* do without presenting anything, which is useful for adjusting UI ahead of time, like hiding an upgrade button for users who would not see a paywall:

```kotlin
val result = Superwall.getPresentationResult(placement = "campaign_trigger")
```

## Controlling the paywall

A few members act on the presented paywall:

```kotlin
Superwall.dismiss()                        // suspend; resumes once dismissed
Superwall.isPaywallPresented               // Boolean
Superwall.latestPaywallInfo                // PaywallInfo?
Superwall.togglePaywallSpinner(isHidden = true)
```

## Preloading

Paywalls preload by default. To take over the timing, disable it and preload yourself:

```kotlin
Superwall.configure(
    apiKey = "pk_your_api_key",
    options = SuperwallOptions(paywalls = PaywallOptions(shouldPreload = false)),
)

// Later
Superwall.preloadAllPaywalls()
Superwall.preloadPaywalls(placementNames = setOf("campaign_trigger"))
```

Next, [manage your users](/docs/kmp/quickstart/user-management).