# 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

# SuperwallDelegate

An interface that handles Superwall lifecycle events and analytics.

> **Note:** Set the delegate using `Superwall.instance.delegate = this` to receive these callbacks. For Java, use `setJavaDelegate()` for better Java interop.

> **Tip:** Use `handleSuperwallEvent(eventInfo)` to track Superwall analytics events in your own analytics platform for a complete view of user behavior.

## Purpose

Provides callbacks for Superwall lifecycle events, analytics tracking, and custom paywall interactions.

## Signature

```kotlin
interface SuperwallDelegate {
    fun subscriptionStatusDidChange(
        from: SubscriptionStatus,
        to: SubscriptionStatus
    ) {}

    fun customerInfoDidChange(
        from: CustomerInfo,
        to: CustomerInfo
    ) {}

    fun userAttributesDidChange(newAttributes: Map<String, Any>) {}
    
    fun handleSuperwallEvent(eventInfo: SuperwallEventInfo) {}
    
    fun handleCustomPaywallAction(name: String) {}
    
    fun willDismissPaywall(paywallInfo: PaywallInfo) {}
    
    fun willPresentPaywall(paywallInfo: PaywallInfo) {}
    
    fun didDismissPaywall(paywallInfo: PaywallInfo) {}
    
    fun didPresentPaywall(paywallInfo: PaywallInfo) {}
    
    fun paywallWillOpenURL(url: String) {}
    
    fun paywallWillOpenDeepLink(url: String) {}
    
    fun handleLog(
        level: LogLevel,
        scope: LogScope,
        message: String,
        info: Map<String, Any>?,
        error: Throwable?
    ) {}
}
```

```java
// Java - Use SuperwallDelegateJava for better Java interop
public interface SuperwallDelegateJava {
    default void subscriptionStatusDidChange(
        SubscriptionStatus from, 
        SubscriptionStatus to
    ) {}

    default void customerInfoDidChange(
        CustomerInfo from,
        CustomerInfo to
    ) {}

    default void userAttributesDidChange(Map<String, Object> newAttributes) {}
    
    default void handleSuperwallEvent(SuperwallEventInfo eventInfo) {}
    
    default void handleCustomPaywallAction(String name) {}
    
    default void willDismissPaywall(PaywallInfo paywallInfo) {}

    default void willPresentPaywall(PaywallInfo paywallInfo) {}

    default void didDismissPaywall(PaywallInfo paywallInfo) {}

    default void didPresentPaywall(PaywallInfo paywallInfo) {}

    default void paywallWillOpenURL(String url) {}

    default void paywallWillOpenDeepLink(String url) {}

    default void handleLog(
        LogLevel level,
        LogScope scope,
        String message,
        Map<String, Object> info,
        Throwable error
    ) {}
}
```

## Parameters

All methods are optional to implement. Key methods include:

<TypeTable
  type="{
  subscriptionStatusDidChange: {
    type: &#x22;from: SubscriptionStatus, to: SubscriptionStatus&#x22;,
    description: &#x22;Called when subscription status changes.&#x22;,
    required: true,
  },
  handleSuperwallEvent: {
    type: &#x22;eventInfo: SuperwallEventInfo&#x22;,
    description: &#x22;Called for all internal analytics events. Use for tracking in your own analytics.&#x22;,
    required: true,
  },
  handleCustomPaywallAction: {
    type: &#x22;name: String&#x22;,
    description: &#x22;Called when user taps elements with `data-pw-custom` tags.&#x22;,
    required: true,
  },
  userAttributesDidChange: {
    type: &#x22;newAttributes: Map<String, Any>&#x22;,
    description: &#x22;Called whenever paywall actions mutate user attributes (for example, forms or surveys).&#x22;,
    required: true,
  },
  customerInfoDidChange: {
    type: &#x22;from: CustomerInfo, to: CustomerInfo&#x22;,
    description: &#x22;Raised when Superwall merges device, web, and external purchase data into a new CustomerInfo snapshot.&#x22;,
    required: true,
  },
  willPresentPaywall: {
    type: &#x22;paywallInfo: PaywallInfo&#x22;,
    description: &#x22;Called before paywall presentation.&#x22;,
    required: true,
  },
  didPresentPaywall: {
    type: &#x22;paywallInfo: PaywallInfo&#x22;,
    description: &#x22;Called after paywall presentation.&#x22;,
    required: true,
  },
  willDismissPaywall: {
    type: &#x22;paywallInfo: PaywallInfo&#x22;,
    description: &#x22;Called before paywall dismissal.&#x22;,
    required: true,
  },
  didDismissPaywall: {
    type: &#x22;paywallInfo: PaywallInfo&#x22;,
    description: &#x22;Called after paywall dismissal.&#x22;,
    required: true,
  },
}"
/>

## Returns / State

All delegate methods return `Unit`. They provide information about Superwall events and state changes.

## Usage

Basic delegate setup:

```kotlin
class MainActivity : AppCompatActivity(), SuperwallDelegate {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Superwall.instance.delegate = this
    }
}
```

Track subscription status changes:

```kotlin
override fun subscriptionStatusDidChange(
    from: SubscriptionStatus,
    to: SubscriptionStatus
) {
    println("Subscription changed from $from to $to")
    updateUI(to)
}
```

Mirror merged purchase history:

```kotlin
override fun customerInfoDidChange(
    from: CustomerInfo,
    to: CustomerInfo
) {
    if (from.activeSubscriptionProductIds != to.activeSubscriptionProductIds) {
        Analytics.track("customer_info_updated", mapOf(
            "old_products" to from.activeSubscriptionProductIds.joinToString(),
            "new_products" to to.activeSubscriptionProductIds.joinToString()
        ))
    }

    refreshEntitlementBadge(to.entitlements.filter { it.isActive }.map { it.id })
}
```

Capture remote attribute changes:

```kotlin
override fun userAttributesDidChange(newAttributes: Map<String, Any>) {
    // Paywall forms or surveys can set attributes directly.
    // Forward them to your analytics platform or local cache.
    analytics.identify(newAttributes)
}
```

Forward analytics events:

```kotlin
override fun handleSuperwallEvent(eventInfo: SuperwallEventInfo) {
    when (val event = eventInfo.event) {
        is SuperwallEvent.PaywallOpen -> {
            Analytics.track("paywall_opened", mapOf(
                "paywall_id" to event.paywallInfo.id,
                "placement" to event.paywallInfo.placement
            ))
        }
        is SuperwallEvent.TransactionComplete -> {
            Analytics.track("subscription_purchased", mapOf(
                "product_id" to event.product.id,
                "paywall_id" to event.paywallInfo.id
            ))
        }
        else -> {
            // Handle other events
        }
    }
}
```

Handle custom paywall actions:

```kotlin
override fun handleCustomPaywallAction(name: String) {
    when (name) {
        "help" -> presentHelpScreen()
        "contact" -> presentContactForm()
        else -> println("Unknown custom action: $name")
    }
}
```

Handle paywall lifecycle:

```kotlin
override fun willPresentPaywall(paywallInfo: PaywallInfo) {
    // Pause video, hide UI, etc.
    pauseBackgroundTasks()
}

override fun didDismissPaywall(paywallInfo: PaywallInfo) {
    // Resume video, show UI, etc.
    resumeBackgroundTasks()
}
```

Java usage:

```java
public class MainActivity extends AppCompatActivity implements SuperwallDelegateJava {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Superwall.getInstance().setJavaDelegate(this);
    }
    
    @Override
    public void subscriptionStatusDidChange(
        SubscriptionStatus from, 
        SubscriptionStatus to
    ) {
        System.out.println("Subscription changed from " + from + " to " + to);
        updateUI(to);
    }

    @Override
    public void customerInfoDidChange(
        CustomerInfo from,
        CustomerInfo to
    ) {
        Logger.i("Superwall", "Customer info updated: " + to.getActiveSubscriptionProductIds());
        syncUserPurchases(to);
    }

    @Override
    public void userAttributesDidChange(Map<String, Object> newAttributes) {
        analytics.identify(newAttributes);
    }
}
```