# 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

# Using your own backend

If you handle subscription logic and receipt validation on your own backend, follow this guide to keep Superwall in sync.

> **Note:** Superwall works out of the box without any backend integration. You only need this guide if your own server is involved in subscription logic.

Common reasons to integrate your own backend include:

* You validate receipts server-side (via the App Store Server API or the Google Play Developer API) instead of trusting the device.
* Your app's entitlements are served from your own API rather than derived from the local receipt.
* You share subscription state across platforms (for example, a subscription purchased on the web that unlocks features in your app).

You can integrate your own backend with Superwall in one of two ways:

1. [**Letting Superwall handle purchases (recommended):**](#letting-superwall-handle-purchases) Keep Superwall's default purchasing behavior and forward completed transactions to your backend for validation and record-keeping.
2. [**Using a purchase controller:**](#using-a-purchase-controller) Take end-to-end control of the purchasing pipeline: you make the purchase, validate it on your server, and tell Superwall the user's subscription status.

> **Warning:** You only need a `PurchaseController` if your backend must be the source of truth for the subscription status Superwall uses on-device. If you just need your server to know about purchases, the first option is much simpler.

## Letting Superwall handle purchases

By default, Superwall handles purchasing, restoring, and subscription status for you — there's nothing to configure. To keep your backend informed, listen for completed transactions and send them to your server for validation.

### 1\. Forward transactions to your backend

Use the [Superwall delegate](/docs/sdk/guides/using-superwall-delegate) to respond to completed transactions and send their identifiers to your backend. Your backend can then verify the purchase directly with Apple or Google:

:::ios
```swift
class SWDelegate: SuperwallDelegate {
  func handleSuperwallEvent(withInfo eventInfo: SuperwallEventInfo) {
    switch eventInfo.event {
    case .transactionComplete(let transaction, let product, _, _):
      Task {
        // Send the transaction to your backend. Your backend can verify it
        // with the App Store Server API using the transaction identifier.
        await sendTransactionToBackend(
          transactionId: transaction?.storeTransactionId,
          originalTransactionId: transaction?.originalTransactionIdentifier,
          productId: product.productIdentifier
        )
      }
    case .transactionRestore(let restoreType, let paywallInfo):
      print("transactionRestore restoreType \(restoreType) \(paywallInfo)")
    default:
      break
    }
  }
}
```
:::

### 2\. Keep your backend in sync with webhooks

Renewals, cancellations, refunds, and billing issues happen off-device, so listening in the app isn't enough. Superwall can send [webhooks](/docs/integrations/webhooks) to your backend for subscription and payment events — use them to keep your server's subscription records up to date without polling store APIs yourself.

> **Warning:** When Superwall handles purchases, don't set `subscriptionStatus` yourself — the SDK updates it automatically after purchases, restores, and receipt checks. If your backend needs to control the status Superwall uses on-device, use a [purchase controller](#using-a-purchase-controller) instead.

## Using a purchase controller

With a `PurchaseController`, you take over purchasing and restoring, and you become responsible for setting `Superwall.shared.subscriptionStatus` from your backend's entitlement state. See [Purchases and Subscription Status](/docs/sdk/guides/advanced-configuration) for a full explanation of this mode.

### 1\. Create a `PurchaseController`

Create a new file called `MyBackendPurchaseController`, then copy and paste the following. Replace `fetchSubscriptionStatusFromBackend()` and `sendTransactionToBackend(...)` with calls to your own API — they're placeholders for whatever your backend exposes:

:::ios
```swift
import SuperwallKit
import StoreKit

enum PurchasingError: LocalizedError {
  case sk2ProductNotFound
  case unverifiedTransaction

  var errorDescription: String? {
    switch self {
    case .sk2ProductNotFound:
      return "Superwall didn't pass a StoreKit 2 product to purchase. Are you sure you're not "
        + "configuring Superwall with a SuperwallOption to use StoreKit 1?"
    case .unverifiedTransaction:
      return "The transaction couldn't be verified."
    }
  }
}

final class MyBackendPurchaseController: PurchaseController {
  // MARK: Sync Subscription Status
  /// Fetches the user's entitlements from your backend and makes sure
  /// Superwall knows about them by setting `Superwall.shared.subscriptionStatus`.
  func syncSubscriptionStatus() async {
    do {
      // Ask your backend which entitlements this user has, e.g. ["pro"].
      let entitlementIds = try await fetchSubscriptionStatusFromBackend()
      let entitlements = Set(entitlementIds.map { Entitlement(id: $0) })
      await MainActor.run {
        if entitlements.isEmpty {
          Superwall.shared.subscriptionStatus = .inactive
        } else {
          Superwall.shared.subscriptionStatus = .active(entitlements)
        }
      }
    } catch {
      // Keep the previous cached status and retry later,
      // e.g. next time the app comes to the foreground.
    }
  }

  // MARK: Handle Purchases
  /// Purchases the product with StoreKit 2, sends the signed transaction to
  /// your backend for validation, and returns the result. This gets called
  /// when someone tries to purchase a product on one of your paywalls.
  func purchase(product: SuperwallKit.StoreProduct) async -> PurchaseResult {
    guard let sk2Product = product.sk2Product else {
      return .failed(PurchasingError.sk2ProductNotFound)
    }
    do {
      let result = try await sk2Product.purchase()
      switch result {
      case .success(let verificationResult):
        guard case .verified(let transaction) = verificationResult else {
          return .failed(PurchasingError.unverifiedTransaction)
        }
        // Send the signed transaction to your backend. Your backend validates
        // it (e.g. with the App Store Server API) and updates the user's
        // entitlements before responding.
        try await sendTransactionToBackend(jws: verificationResult.jwsRepresentation)
        await transaction.finish()
        await syncSubscriptionStatus()
        return .purchased
      case .userCancelled:
        return .cancelled
      case .pending:
        return .pending
      @unknown default:
        return .cancelled
      }
    } catch {
      return .failed(error)
    }
  }

  // MARK: Handle Restores
  /// Restores purchases and returns `.restored`, unless an error is thrown.
  /// This gets called when someone tries to restore purchases on one of your paywalls.
  func restorePurchases() async -> RestorationResult {
    do {
      try await AppStore.sync()
      // Re-fetch entitlements from your backend after the restore.
      await syncSubscriptionStatus()
      return .restored
    } catch {
      return .failed(error)
    }
  }
}
```
:::

As discussed in [Purchases and Subscription Status](/docs/sdk/guides/advanced-configuration), this `PurchaseController` is responsible for handling the subscription-related logic. Take a few moments to look through the code to understand how it does this.

### 2\. Configure Superwall

Initialize an instance of `MyBackendPurchaseController` and pass it in to `Superwall.configure(apiKey:purchaseController:)`:

:::ios
```swift
let purchaseController = MyBackendPurchaseController()

Superwall.configure(
  apiKey: "MY_API_KEY",
  purchaseController: purchaseController
)

// Make sure we sync the subscription status on first load
Task {
  await purchaseController.syncSubscriptionStatus()
}
```
:::

### 3\. Sync the subscription status

Until you set the subscription status, it's `.unknown` — and paywall presentation is automatically delayed until it changes. Call `syncSubscriptionStatus()` when the app launches and whenever your backend's entitlement state changes, so Superwall always knows whether to show a paywall.

## Keeping the status in sync across launches and devices

Your backend is the source of truth, so treat the on-device status as a cache of it:

* **Identify users consistently.** Call `identify` with the same user ID your backend uses so entitlements follow the user across devices. See [User Management](/docs/sdk/quickstart/user-management).
* **Fetch entitlements on launch.** Call your backend's entitlement endpoint when the app launches (and when it returns to the foreground) and update the subscription status with the result. `subscriptionStatus` is cached between app launches, so the previous value is used until your fetch completes.
* **Make server notifications your source of truth.** Renewals, cancellations, refunds, and billing issues happen off-device. Subscribe your backend to [App Store Server Notifications](https://developer.apple.com/documentation/appstoreservernotifications) and Google Play's [Real-time Developer Notifications](https://developer.android.com/google/play/billing/rtdn-reference) so its entitlement records stay correct — the app then picks up changes the next time it syncs.