# 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

# PaywallPresentationHandler

A handler class that provides status updates for paywall presentation in register() calls.

> **Info:** Use this handler when you need fine-grained control over paywall events for a specific [`register()`](/docs/ios/sdk-reference/register) call, rather than global events via [`SuperwallDelegate`](/docs/ios/sdk-reference/SuperwallDelegate).

> **Note:** This handler is specific to the individual `register()` call. For global paywall events across your app, use [`SuperwallDelegate`](/docs/ios/sdk-reference/SuperwallDelegate) instead.

## Purpose

Provides callbacks for paywall lifecycle events when using [`register()`](/docs/ios/sdk-reference/register) with a specific handler instance.

## Signature

```swift
@objcMembers
public final class PaywallPresentationHandler: NSObject {
  public func onPresent(_ handler: @escaping (PaywallInfo) -> Void)
  public func onWillDismiss(_ handler: @escaping (PaywallInfo, PaywallResult) -> Void)
  public func onDismiss(_ handler: @escaping (PaywallInfo, PaywallResult) -> Void)
  public func onSkip(_ handler: @escaping (PaywallSkippedReason) -> Void)
  public func onError(_ handler: @escaping (Error) -> Void)
  public func onCustomCallback(_ handler: @escaping (CustomCallback) async -> CustomCallbackResult)
}
```

## Parameters

<TypeTable
  type="{
  onPresent: {
    type: &#x22;handler: (PaywallInfo) -> Void&#x22;,
    description: &#x22;Sets a handler called when the paywall is presented.&#x22;,
    required: true,
  },
  onWillDismiss: {
    type: &#x22;handler: (PaywallInfo, PaywallResult) -> Void&#x22;,
    description: &#x22;Sets a handler called when the paywall will be dismissed. Available in version 4.9.0+.&#x22;,
    required: true,
  },
  onDismiss: {
    type: &#x22;handler: (PaywallInfo, PaywallResult) -> Void&#x22;,
    description: &#x22;Sets a handler called when the paywall is dismissed.&#x22;,
    required: true,
  },
  onSkip: {
    type: &#x22;handler: (PaywallSkippedReason) -> Void&#x22;,
    description: &#x22;Sets a handler called when paywall presentation is skipped.&#x22;,
    required: true,
  },
  onError: {
    type: &#x22;handler: (Error) -> Void&#x22;,
    description: &#x22;Sets a handler called when an error occurs during presentation.&#x22;,
    required: true,
  },
  onCustomCallback: {
    type: &#x22;handler: (CustomCallback) async -> CustomCallbackResult&#x22;,
    description: &#x22;Sets an async handler called when the paywall requests a custom callback. The handler receives a CustomCallback with a name and optional variables, and returns a CustomCallbackResult indicating success or failure. Available in version 4.12.10+.&#x22;,
    required: false,
  },
}"
/>

## Returns / State

Each method returns `Void` and configures the handler for the specific paywall lifecycle event.

## Usage

Basic handler setup:

```swift
func registerFeatureWithHandler() {
  let handler = PaywallPresentationHandler()
  
  handler.onPresent { paywallInfo in
    print("Paywall presented: \(paywallInfo.id)")
    // Pause background tasks, analytics, etc.
  }
  
  handler.onWillDismiss { paywallInfo, result in
    print("Paywall will dismiss: \(paywallInfo.id)")
    // Prepare for dismissal, save state, etc.
  }
  
  handler.onDismiss { paywallInfo, result in
    print("Paywall dismissed with result: \(result)")
    
    switch result {
    case .purchased:
      showSuccessMessage()
    case .cancelled:
      showPromotionalOffer()
    case .restored:
      updateUIForActiveSubscription()
    }
  }
  
  Superwall.shared.register(
    placement: "premium_feature",
    params: ["source": "feature_screen"],
    handler: handler
  ) {
    unlockPremiumFeature()
  }
}
```

Handle skip and error cases:

```swift
let handler = PaywallPresentationHandler()

handler.onSkip { reason in
  print("Paywall skipped: \(reason)")
  
  switch reason {
  case .userIsSubscribed:
    proceedToFeature()
  case .holdout:
    proceedToFeature()
  default:
    break
  }
}

handler.onError { error in
  print("Paywall error: \(error)")
  showErrorAlert(error)
}
```

Handle custom callbacks (4.12.10+):

```swift
let handler = PaywallPresentationHandler()

handler.onCustomCallback { callback in
  print("Custom callback: \(callback.name)")

  // Perform your async work based on the callback name
  do {
    let result = try await performAction(named: callback.name, with: callback.variables)
    return .success(data: ["result": result])
  } catch {
    return .failure(data: ["error": error.localizedDescription])
  }
}

Superwall.shared.register(
  placement: "premium_feature",
  handler: handler
) {
  unlockPremiumFeature()
}
```

Method chaining style:

```swift
func registerWithChaining() {
  let handler = PaywallPresentationHandler()
    .onPresent { _ in pauseVideo() }
    .onDismiss { _, result in 
      resumeVideo()
      handlePurchaseResult(result)
    }
    .onError { error in showAlert(error) }
  
  Superwall.shared.register(
    placement: "remove_ads",
    handler: handler
  ) {
    hideAdsFromUI()
  }
}
```