# Flutter Documentation # 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 # Changelog Release notes for the Superwall Flutter SDK # CHANGELOG The changelog for `Superwall`. Also see the [releases](https://github.com/superwall/Superwall-Flutter/releases) on GitHub. ## 2.4.12 ### Enhancements * Updates Android SDK to 2.7.11 [View Android SDK release notes](https://github.com/superwall/Superwall-Android/releases/tag/2.7.11). * Updates iOS SDK to 4.14.2 [View iOS SDK release notes](https://github.com/superwall/Superwall-iOS/releases/tag/4.14.2). * Adds `preloadDeviceOverrides` option to `PaywallOptions` to override `shouldPreload` per device tier (Android only) * Exports `PresentationResult`, `StoreTransaction`, `TransactionProduct` from the public package * Fixes `PurchaserInfo.storeIdentifiers` leaking the internal Pigeon type instead of the public `StoreIdentifiers` sealed class ## 2.4.11 ### Enhancements * Updates iOS SDK to 4.14.1 [View iOS SDK release notes](https://github.com/superwall/Superwall-iOS/releases/tag/4.14.1). * Updates Android SDK to 2.7.8 [View Android SDK release notes](https://github.com/superwall/Superwall-Android/releases/tag/2.7.8). * Adds `appstackId` integration attribute for Appstack integration support * Adds `testModeBehavior` option to `SuperwallOptions` to control when the SDK enters test mode (automatic, whenEnabledForUser, never, always) * Adds `shouldObservePurchases` option to `SuperwallOptions` to observe purchases made outside of Superwall * Adds `shouldBypassAppTransactionCheck` option to `SuperwallOptions` (iOS only) * Adds `maxConfigRetryCount` option to `SuperwallOptions` (iOS only) * Adds `useMockReviews` option to `SuperwallOptions` (Android only) ## 2.4.10 ## Updates * Updates pigeon to 26.1.5 to support analyzer 9.+.+ versions ## 2.4.9 ## Enhancements * Updates iOS SDK to 4.13.0 [View iOS SDK release notes](https://github.com/superwall/Superwall-iOS/releases/tag/4.13.0). * Updates Android SDK to 2.7.1 [View Android SDK release notes](https://github.com/superwall/Superwall-Android/releases/tag/2.7.1). ## 2.4.8 ## Enhancements * Adds `onCustomCallback` method to `PaywallPresentationHandler` that allows invoking custom callbacks and passing data from and to the paywall * Updates iOS SDK to 4.12.10 [View iOS SDK release notes](https://github.com/superwall/Superwall-iOS/releases/tag/4.12.10). * Updates Android SDK to 2.7.0 [View Android SDK release notes](https://github.com/superwall/Superwall-Android/releases/tag/2.7.0). ### Fixes * Fixes an issue where `IdentityOptions` passed to `identify()` were not being sent to the native SDK due to a variable shadowing bug. ## 2.4.7 ### Enhancements * Updates iOS SDK to 4.12.7 [View iOS SDK release notes](https://github.com/superwall/Superwall-iOS/releases/tag/4.12.7). * Updates Android SDK to 2.6.8 [View Android SDK release notes](https://github.com/superwall/Superwall-Android/releases/tag/2.6.8). ## 2.4.6 ## Enhancements * Adds `isActive` convenience property on the `SubscriptionStatus` class * Adds support for `SuperwallOptions.paywalls.onBackPressed` callback to react to back button presses on Android ## 2.4.5 ### Enhancements * Updates iOS SDK to 4.10.6 [View iOS SDK release notes](https://github.com/superwall/Superwall-iOS/releases/tag/4.10.6). * Updates Android SDK to 2.6.5 [View Android SDK release notes](https://github.com/superwall/Superwall-Android/releases/tag/2.6.5). ## 2.4.4 ## Enhancements * Adds `Superwall.shared.consume(purchaseToken)` method to allow consuming inapp purchases * Updates Android SDK to 2.6.2 [View Android SDK release notes](https://github.com/superwall/Superwall-Android/releases/tag/2.6.2). ## 2.4.3 ### Enhancements * Updates Android SDK to 2.6.1 [View Android SDK release notes](https://github.com/superwall/Superwall-Android/releases/tag/2.6.1). ## 2.4.2 ### Enhancements * Exposes `Superwall.shared.overrideProductsByName` variable. ## 2.4.1 ### Enhancements * Updates iOS SDK to 4.7.0 [View iOS SDK release notes](https://github.com/superwall/Superwall-iOS/releases/tag/4.7.0). * Updates Android SDK to 2.5.1 [View Android SDK release notes](https://github.com/superwall/Superwall-Android/releases/tag/2.5.1). Note: This contains an update to Play Billing 8. * Adds `overrideProductsByName` to `PaywallOptions` - allows globally overriding products on any paywall by product name. * Adds `shouldShowWebPurchaseConfirmationAlert` to `PaywallOptions` - shows localized alert confirming successful web checkout purchases (iOS only). ### Fixes * Fixes missing paywall info ## 2.4.0 ## Enhancements * Updates Android SDK to 2.3.1 [View Android SDK release notes](https://github.com/superwall/Superwall-Android/releases/tag/2.3.1). ## 2.3.5 ### Fixes * Adds `params` mapping back to `EventInfo` for easier forwarding to analytics ## 2.3.4 ### Enhancements * Updates iOS SDK to 4.5.2 [View iOS SDK release notes](https://github.com/superwall/Superwall-iOS/releases/tag/4.5.2). * Updates Android SDK to 2.2.3 [View Android SDK release notes](https://github.com/superwall/Superwall-Android/releases/tag/2.2.3). ### Fixes * Fixes issue where `shouldShowWebRestorationAlert` wasn't getting through to the iOS `PaywallOptions`. ## 2.3.3 ### Enhancements * Exposes `shouldShowWebRestorationAlert` in `PaywallOptions`. ## 2.3.2 ### Enhancements * Expose `getPresentationResult` method * Updates Android SDK to 2.2.2 [View Android SDK release notes](https://github.com/superwall/Superwall-Android/releases/tag/2.2.2). ## 2.3.1 ### Enhancements * Updates Android SDK to 2.1.2 [View Android SDK release notes](https://github.com/superwall/Superwall-Android/releases/tag/2.1.2). ### Fixes * Fixes issue with `registerPlacement` blocking ## 2.3.0 ### Enhancements * Exposes `ConfirmedAssignments` and `StoreProduct` to `superwallkit_flutter`. * Adds `Superwall.shared.getDeviceAttributes()`, which returns the device attributes that are used when evaluating audience filters. * Updates iOS SDK to 4.4.1 [View iOS SDK release notes](https://github.com/superwall/Superwall-iOS/releases/tag/4.4.1). * Updates Android SDK to 2.1.1 [View Android SDK release notes](https://github.com/superwall/Superwall-Android/releases/tag/2.1.1). ### Fixes * Fixes issue where `SuperwallEventInfo` didn't contain all expected variables. * Fixes issue when building for app that deploys with a minimum version of iOS 13. ## 2.2.2 ### Fixes * Fixes issue with paywall presentation handlers not working. ## 2.2.1 ### Fixes * Fixes issue with iOS side of project not building. ## 2.2.0 ### Fixes * Fixes missing Event info ### Enhancements * Updates iOS SDK to 4.3.11 [View iOS SDK release notes](https://github.com/superwall/Superwall-iOS/releases/tag/4.3.11) * Updates Android SDK to 2.1.0 [View Android SDK release notes](https://github.com/superwall/Superwall-Android/releases/tag/2.1.0) ## 2.2.0-beta.2 ### Enhancements * Updates iOS SDK to 4.3.8 [View iOS SDK release notes](https://github.com/superwall/Superwall-iOS/releases/tag/4.3.8) ## 2.2.0-beta.1 ### Enhancements * Adds support for web checkout. * Updates kotlin version of the underlying Android SDK to 2.0.21 * Updates iOS SDK to 4.3.5 [View iOS SDK release notes](https://github.com/superwall/Superwall-iOS/releases/tag/4.3.5) * Updates iOS SDK to 2.1.0-beta.2 [View iOS SDK release notes](https://github.com/superwall/Superwall-Android/releases/tag/2.1.0-beta.2) ## 2.1.0 ### Enhancements * Updates Android SDK to 2.0.8 [View Android SDK release notes](https://github.com/superwall/Superwall-Android/releases/tag/2.0.8) * Updates iOS SDK to 4.3.0 [View iOS SDK release notes](https://github.com/superwall/Superwall-iOS/releases/tag/4.3.0) ## 2.1.0-beta.2 ### Enhancements * Adds `restorePurchases` function * Updates Android SDK to 2.0.6 [View Android SDK release notes](https://github.com/superwall/Superwall-Android/releases/tag/2.0.6) * Updates iOS SDK to 4.2.0 [View iOS SDK release notes](https://github.com/superwall/Superwall-iOS/releases/tag/4.2.0) ## 2.1.0-beta.1 ### Enhancements * Reworks the SDK's beneath the surface to rely on Pigeon codegen * Improves stability * Improved correctness of the SDK ### Note: * This could include some potentially breaking changes. If you find some, please open an issue or reach out to the team. ## 2.0.8 ### Enhancements * Updates iOS SDK to 4.0.6 [View iOS SDK release notes](https://github.com/superwall/Superwall-iOS/releases/tag/4.0.6) * Updates Android SDK to 2.0.5 [View Android SDK release notes](https://github.com/superwall/Superwall-Android/releases/tag/2.0.5) ## 2.0.7 ### Fixes * Fixes issue when accessing `presentedByPlacement*` properties from a custom placement ## 2.0.6 ## Enhancements * Adds a subscription stream example in the example app ## Fixes * Fixes issue with second subscriber not receiving the `SubscriptionStatus` broadcast stream * Fixes issue with `PurchasedPaywallResult` not properly deserialising in `onDismiss` ## 2.0.5 ### Enhancements * Update `SuperwallPlacement` to `SuperwallEvent` * Update `PlacementType` to `EventType` * Updates Android SDK to 2.0.3 [View Android SDK release notes](https://github.com/superwall/Superwall-Android/releases/tag/2.0.3) * Updates iOS SDK to 4.0.3 [View iOS SDK release notes](https://github.com/superwall/Superwall-iOS/releases/tag/4.0.3) ## 2.0.4 ### Enhancements * Updates onDismissHandler to now receive a PaywallResult also * Updates Android SDK to 2.0.2 [View Android SDK release notes](https://github.com/superwall/Superwall-Android/releases/tag/2.0.2) * `handleSuperwallPlacement(SuperwallPlacementInfo eventInfo)` has been replaced with `handleSuperwallEvent(SuperwallEventtInfo eventInfo)` ## 2.0.3 ### Fixes * Fixes BridgeInstance crashes * Adds awaiting on shared instance of BridgeCreator to avoid crashes when plugin is yet to attach ## 2.0.2 * Updates iOS SDK to 1.5.4 [View iOS SDK release notes](https://github.com/superwall-me/Superwall-iOS/releases/tag/4.0.1) * Updates Android SDK to 2.0.1 [View Android SDK release notes](https://github.com/superwall-me/Superwall-Android/releases/tag/2.0.1) ## 2.0.1 * Upgrades iOS SDK to 4.0.0 [View iOS SDK release notes](https://github.com/superwall-me/Superwall-iOS/releases/tag/4.0.0) ## 2.0.0 ### * Adds entitlements to subscriptions, allowing you to set them using: * `await Superwall.shared.setSubscriptionStatus(status)` by passing in one of: * `SubscriptionStatusActive(entitlements: entitlements))` * Note: Passing in empty entitlements will set the subscription status to inactive * `SubscriptionStatusInactive` * `SubscriptionStatusUnknown` * Subscribing to subscription status updates via stream `Superwall.shared.subscriptionStatus` * `SuperwallBuilder` which will automatically update the state whenever subscription status changes * `Superwall.shared.registerEvent` has now been replaced with `Superwall.shared.registerPlacement` * `handleSuperwallEvent(SuperwallEventInfo eventInfo)` has been replaced with `handleSuperwallPlacement(SuperwallPlacementInfo placementInfo)` * `PaywallPresentationHandler.onDismiss` now has 2 arguments, `PaywallInfo` and `PaywallResult` ## 2.0.0-alpha.1 * Adds entitlements to subscriptions, allowing you to set them using: * `await Superwall.shared.setSubscriptionStatus(status)` by passing in one of: * `SubscriptionStatusActive(entitlements: entitlements))` * Note: Passing in empty entitlements will set the subscription status to inactive * `SubscriptionStatusInactive` * `SubscriptionStatusUnknown` * Subscribing to subscription status updates via stream `Superwall.shared.subscriptionStatus` * `SuperwallBuilder` which will automatically update the state whenever subscription status changes * `Superwall.shared.registerEvent` has now been replaced with `Superwall.shared.registerPlacement` * `handleSuperwallEvent(SuperwallEventInfo eventInfo)` has been replaced with `handleSuperwallPlacement(SuperwallPlacementInfo placementInfo)` ## 1.3.11 ### Fixes * Fixes BridgeInstance crashes * Adds awaiting on shared instance of BridgeCreator to avoid crashes when plugin is yet to attach ## 1.3.10 ### Fixes * Fixes an issue on iOS where the plugin could get registered multiple times resulting in a `MissingPluginException`. ## 1.3.9 ### Fixes * Fixes a compatibility issue with `WorkManager` and other isolate-running libraries where the plugin would detach and cause a `Missing Activity` or `BridgeCreator` exception ### Enhancements * Updates Android SDK to 1.5.4 [View Android SDK release notes](https://github.com/superwall-me/Superwall-Android/releases/tag/1.5.4) ## 1.3.8 ### Fixes * Removes unnecessary date comparison from PurchaseController example code. ## 1.3.7 ### Enhancements * Upgrades iOS SDK to 3.12.1 [View iOS SDK release notes](https://github.com/superwall-me/Superwall-iOS/releases/tag/3.12.1) ## 1.3.6 ### Enhancements * Updates Android SDK to 1.5.1 [View Android SDK release notes](https://github.com/superwall-me/Superwall-Android/releases/tag/1.5.1) ## 1.3.5 ### Enhancements * Upgrades iOS SDK to 3.12.0 [View iOS SDK release notes](https://github.com/superwall-me/Superwall-iOS/releases/tag/3.12.0) * Updates Android SDK to 1.5.0 [View Android SDK release notes](https://github.com/superwall-me/Superwall-Android/releases/tag/1.5.0) ### Fixes * Fixes issue with `PaywallInfoBridge` and other bridges throwing NPE when reattaching to activities from deep sleep. ## 1.3.4 ### Enhancements * Adds `setLocaleIdentifier(_:)` and `getLocaleIdentifier()`. ## 1.3.3 * Upgrades Android SDK to 1.3.1 [View Android SDK release notes](https://github.com/superwall-me/Superwall-Android/releases/tag/1.3.1) * This fixes the issue when using Superwall with some SDK's would cause a crash (i.e. Smartlook SDK) ## 1.3.2 * Updates project linting and formatting to be consistent with the Flutter style * Removes rogue logging statements by matching them with the provided Log level * Adds `passIdentifiersToPlayStore` to `SuperwallOptions` which allows you to pass user identifiers to the Play Store purchases as account identifiers. This is useful for tracking user purchases in the Play Store console. ## 1.3.1 ### Enhancements * Upgrades Android SDK to 1.3.0 [View Android SDK release notes](https://github.com/superwall-me/Superwall-Android/releases/tag/1.3.0) * Upgrades iOS SDK to 3.10.1 [View iOS SDK release notes](https://github.com/superwall-me/Superwall-iOS/releases/tag/3.10.1) * Adds `confirmAllAssignments` method to `Superwall` which confirms assignments for all placements and returns an array of all confirmed experiment assignments. Note that the assignments may be different when a placement is registered due to changes in user, placement, or device parameters used in audience filters. ## 1.3.0 ### Enhancements * Upgrades iOS SDK to 3.9.1. [View iOS SDK release notes](https://github.com/superwall-me/Superwall-iOS/releases/tag/3.9.1) * Upgrades Android SDK to 1.2.7 [View Android SDK release notes](https://github.com/superwall-me/Superwall-Android/releases/tag/1.2.7) * Exposes the `SuperwallOption` `collectAdServicesAttribution` for iOS. When `true`, this collects the AdServices attribute token, which will be process by our backend. Adds `adServicesTokenRequest_start`, `adServicesTokenRequest_complete`, and `adServicesTokenRequest_fail`. * Exposes `getConfigurationStatus()`, which replaces `getIsConfigured()`. This returns either `ConfigurationStatusPending`, `ConfigurationStatusConfigured`, or `ConfigurationStatusFailed`. ## 1.2.2 ### Enhancements * Upgrades Android SDK to 1.2.4 [View Android SDK release notes](https://github.com/superwall-me/Superwall-Android/releases/tag/1.2.4) ### Fixes * Reapply the single bridge instance limit to fix issues with `setDelegate` where plugins with multiple isolates are used * Fixes stateful bridges by applying new state when invoking create, resolving issues seen in #23 ## 1.2.1 ### Enhancements * Upgrades Android SDK to 1.2.3 [View Android SDK release notes](https://github.com/superwall-me/Superwall-Android/releases/tag/1.2.3) * Upgrades iOS SDK to 3.7.2. [View iOS SDK release notes](https://github.com/superwall-me/Superwall-iOS/releases/tag/3.7.2) ## 1.2.0 ### Enhancements * Upgrades Android SDK to 1.2.0 [View Android SDK release notes](https://github.com/superwall-me/Superwall-Android/releases/tag/1.2.0) ### Fixes * Updates `compileSDKVersion` to 34 fixing build issues on Android. ## 1.1.9 ### Enhancements * Upgrades Android SDK to 1.1.8. [View Android SDK release notes](https://github.com/superwall-me/Superwall-Android/releases/tag/1.1.8) ### Fixes * Bumps minimum Kotlin version to 1.8.0 and Android Gradle Plugin to 8.1.0 to be able to be compatible with the latest Android SDK. This was necessary for important bug fixes. * SW-2868: Fixes transaction issues caused by hot restart. ## 1.1.8 ### Enhancements * Upgrades iOS SDK to 3.6.6. [View iOS SDK release notes](https://github.com/superwall-me/Superwall-iOS/releases/tag/3.6.6) * Upgrades Android SDK to 1.1.7. [View Android SDK release notes](https://github.com/superwall-me/Superwall-Android/releases/tag/1.1.7) ## 1.1.7 ### Enhancements * Upgrades iOS SDK to 3.6.5. [View iOS SDK release notes](https://github.com/superwall-me/Superwall-iOS/releases/tag/3.6.5) ## 1.1.6 ### Enhancements * Upgrades Android SDK to 1.1.6. [View Android SDK release notes](https://github.com/superwall-me/Superwall-Android/releases/tag/1.1.6) ## 1.1.5 ### Enhancements * Upgrades Android SDK to 1.1.5. [View Android SDK release notes](https://github.com/superwall-me/Superwall-Android/releases/tag/1.1.5) ## 1.1.4 ### Enhancements * Upgrades Android SDK to 1.1.4. [View Android SDK release notes](https://github.com/superwall-me/Superwall-Android/releases/tag/1.1.4) * Upgrades iOS SDK to 3.6.2. [View iOS SDK release notes](https://github.com/superwall-me/Superwall-iOS/releases/tag/3.6.2) * Adds `identityAlias` `SuperwallEvent`. ### Fixes * Updates the UUID framework dependency to the latest version to prevent conflicts. ## 1.1.3 ### Fixes * Fixes crash that occurred if an Android foreground service was started and the app was relaunched from a cold start. ## 1.1.2 ### Enhancements * Upgrades Android SDK to 1.1.2. [View Android SDK release notes](https://github.com/superwall-me/Superwall-Android/releases/tag/1.1.2) * Upgrades iOS SDK to 3.6.1. [View iOS SDK release notes](https://github.com/superwall-me/Superwall-iOS/releases/tag/3.6.1) ## 1.1.1 ### Fixes * Fixes `LocalNotification` issue that was trying to initialise an `int` with a `double`. ## 1.1.0 ### Enhancements * Upgrades Android SDK to 1.1.1. [View Android SDK release notes](https://github.com/superwall-me/Superwall-Android/releases/tag/1.1.1) * Upgrades iOS SDK to 3.6.0. [View iOS SDK release notes](https://github.com/superwall-me/Superwall-iOS/releases/tag/3.6.0) ## 1.0.1 ### Fixes * Fixes issue with invalid imports due to a breaking change on Android. ## 1.0.0 ### Enhancements * Upgrades Android SDK to 1.0.0. [View Android SDK release notes](https://github.com/superwall-me/Superwall-Android/releases/tag/1.0.0) * Upgrades iOS SDK to 3.5.0. [View iOS SDK release notes](https://github.com/superwall-me/Superwall-iOS/releases/tag/3.5.0) # 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 # Cohorting in 3rd Party Tools To easily view Superwall cohorts in 3rd party tools, we recommend you set user attributes based on the experiments that users are included in. You can also use custom placements for creating analytics events for actions such as interacting with an element on a paywall. :::android ```kotlin Kotlin override fun handleSuperwallEvent(eventInfo: SuperwallEventInfo) { when(eventInfo.event) { is SuperwallEvent.TriggerFire -> { MyAnalyticsService.shared.setUserAttributes( mapOf( "sw_experiment_${eventInfo.params.get("experiment_id").toString()}" to true, "sw_variant_${eventInfo.params.get("variant_id").toString()}" to true ) ) } else -> {} } } ``` ::: :::flutter ```dart Flutter @override void handleSuperwallEvent(SuperwallEventInfo eventInfo) async { final experimentId = eventInfo.params?['experiment_id']; final variantId = eventInfo.params?['variant_id']; switch (eventInfo.event.type) { case EventType.triggerFire: MyAnalyticsService.shared.setUserAttributes({ "sw_experiment_$experimentId": true, "sw_variant_$variantId": true }); break; default: break; } } ``` ::: :::expo ```typescript React Native handleSuperwallEvent(eventInfo: SuperwallEventInfo) { const experimentId = eventInfo.params?['experiment_id'] const variantId = eventInfo.params?['variant_id'] if (!experimentId || !variantId) { return } switch (eventInfo.event.type) { case EventType.triggerFire: MyAnalyticsService.shared.setUserAttributes({ `sw_experiment_${experimentId}`: true, `sw_variant_${variantId}`: true }); break; default: break; } } ``` ::: Once you've set this up, you can easily ask for all users who have an attribute `sw_experiment_1234` and breakdown by both variants to see how users in a Superwall experiment behave in other areas of your app. # 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 # Custom Paywall Analytics Learn how to log events from paywalls, such as a button tap or product change, to forward to your analytics service. You can create customized analytics tracking for any paywall event by using custom placements. With them, you can get callbacks for actions such as interacting with an element on a paywall sent to your [Superwall delegate](/docs/sdk/guides/using-superwall-delegate). This can be useful for tracking how users interact with your paywall and how that affects their behavior in other areas of your app. For example, in the paywall below, perhaps you're interested in tracking when people switch the plan from "Standard" and "Pro": ![](/docs/images/3pa_cp_2.jpeg) You could create a custom placement [tap behavior](/docs/dashboard/dashboard-creating-paywalls/paywall-editor-styling-elements#tap-behaviors) which fires when a segment is tapped: ![](/docs/images/3pa_cp_1.jpeg) Then, you can listen for this placement and forward it to your analytics service: ```swift Swift extension SuperwallService: SuperwallDelegate { func handleSuperwallEvent(withInfo eventInfo: SuperwallEventInfo) { switch eventInfo.event { case let .customPlacement(name, params, paywallInfo): // Prints out didTapPro or didTapStandard print("\(name) - \(params) - \(paywallInfo)") MyAnalyticsService.shared.send(event: name, params: params) default: print("Default event: \(eventInfo.event.description)") } } } ``` For a walkthrough example, check out this [video on YouTube](https://youtu.be/4rM1rGRqDL0). # 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 # 3rd Party Analytics ### Hooking up Superwall events to 3rd party tools SuperwallKit automatically tracks some internal events. You can [view the list of events here](/docs/sdk/guides/3rd-party-analytics/tracking-analytics). We encourage you to also track them in your own analytics by implementing the [Superwall delegate](/docs/sdk/guides/using-superwall-delegate). Using the `handleSuperwallEvent(withInfo:)` function, you can forward events to your analytics service: :::flutter ```dart @override void handleSuperwallEvent(SuperwallEventInfo eventInfo) async { print("handleSuperwallEvent: $eventInfo"); // Example usage... switch (eventInfo.event.type) { case EventType.appOpen: print("appOpen event"); case EventType.deviceAttributes: print("deviceAttributes event: ${eventInfo.event.deviceAttributes} "); case EventType.paywallOpen: final paywallInfo = eventInfo.event.paywallInfo; print("paywallOpen event: ${paywallInfo} "); if (paywallInfo != null) { final identifier = await paywallInfo.identifier; print("paywallInfo.identifier: ${identifier} "); final productIds = await paywallInfo.productIds; print("paywallInfo.productIds: ${productIds} "); } default: break; } } ``` :::
> **Note:** You might also want to set user attribute to allow for > [Cohorting in 3rd Party Tools](/docs/sdk/guides/3rd-party-analytics/cohorting-in-3rd-party-tools). Alternatively, if you want typed versions of all these events with associated values, you can access them via `eventInfo.event`: :::flutter ```dart @override void handleSuperwallEvent(SuperwallEventInfo eventInfo) async { // Example usage... switch (eventInfo.event.type) { case PlacementType.appOpen: print("appOpen event"); case PlacementType.deviceAttributes: print("deviceAttributes event: ${eventInfo.event.deviceAttributes} "); case PlacementType.paywallOpen: final paywallInfo = eventInfo.event.paywallInfo; print("paywallOpen event: ${paywallInfo} "); if (paywallInfo != null) { final identifier = await paywallInfo.identifier; print("paywallInfo.identifier: ${identifier} "); final productIds = await paywallInfo.productIds; print("paywallInfo.productIds: ${productIds} "); } default: break; } } ``` ::: > **Info:** Wanting to use events to see which product was purchased on a paywall? Check out this > [doc](/docs/sdk/guides/advanced/viewing-purchased-products). # 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 # Superwall Events The SDK automatically tracks some events, which power the charts in the dashboard. We encourage you to track them in your own analytics as described in [3rd Party Analytics](..). The following Superwall events can be used as placements to present paywalls: * `app_install` * `app_launch` * `deepLink_open` * `session_start` * `paywall_decline` * `transaction_fail` * `transaction_abandon` * `survey_response` For more info about how to use these, check out [how to add them using a Placement](/docs/dashboard/dashboard-campaigns/campaigns-placements#adding-a-placement). The full list of events is as follows: | **Event Name** | **Action** | **Parameters** | | ---------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `adServicesTokenRequest_complete` | When the AdServices token request finishes. | `["token": String]` | | `adServicesTokenRequest_fail` | When the AdServices token request fails. | `["error_message": String]` | | `adServicesTokenRequest_start` | When the AdServices token request starts. | None | | `app_close` | Anytime the app leaves the foreground. | Same as `app_install` | | `app_install` | When the SDK is configured for the first time. | `["is_superwall": true, "app_session_id": String, "using_purchase_controller": Bool]` | | `app_launch` | When the app is launched from a cold start. | Same as `app_install` | | `app_open` | Anytime the app enters the foreground. | Same as `app_install` | | `attribution_match` | When install attribution is resolved or fails to resolve. | `["provider": String, "matched": Bool, "source": String?, "confidence": String?, "match_score": Double?, "reason": String?]` | | `config_attributes` | When the attributes affecting Superwall's configuration are set or changed. | The SDK configuration options + `["using_purchase_controller": Bool, "has_delegate": Bool]` | | `config_fail` | When the Superwall configuration fails to be retrieved. | `["error_message": String]` | | `config_refresh` | When the Superwall configuration is refreshed. | `["config_build_id": String, "retry_count": Int, "cache_status": String, "fetch_duration": Double]` | | `confirm_all_assignments` | When all experiment assignments are confirmed. | None | | `custom_placement` | When the user taps on an element in the paywall that has a `custom_placement` action. | `["name": String, "params": [String: Any], "paywallInfo": PaywallInfo]` | | `customerInfo_didChange` | When the user's customer info changes. | `["from": String, "to": String]` (JSON snapshots of the entitlements before and after the change) | | [`deepLink_open`](/docs/dashboard/dashboard-campaigns/campaigns-standard-placements#using-the-deeplink-open-event) | When a user opens the app via a deep link. | `["url": String, "path": String", "pathExtension": String, "lastPathComponent": String, "host": String, "query": String, "fragment": String]` + any query parameters in the deep link URL | | `device_attributes` | When device attributes are sent to the backend every session. | Includes `app_session_id`, `app_version`, `os_version`, `device_model`, `device_locale`, and various hardware/software details. | | `enrichment_complete` | When the enrichment request completes. | `user_` and `device_` entries for each returned enrichment value | | `enrichment_fail` | When the enrichment request fails. | None | | `enrichment_start` | When the enrichment request starts. | None | | `first_seen` | When the user is first seen in the app, regardless of login status. | Same as `app_install` | | `freeTrial_start` | When a user completes a transaction for a subscription product with an introductory offer. | Same as `subscription_start` | | `identity_alias` | When the user's identity aliases after calling `identify`. | None | | `integration_attributes` | When the integration attributes are set. | The provided integration attributes | | `networkDecoding_fail` | When a response from the network fails to decode. | `["request_url": String, "response": String]` | | `nonRecurringProduct_purchase` | When the user purchases a non-recurring product. | Same as `subscription_start` | | `paywall_close` | When a paywall is closed (either manually or after a transaction succeeds). | \[“paywall\_webview\_load\_complete\_time”: String?, “paywall\_url”: String, “paywall\_response\_load\_start\_time”: String?, “paywall\_products\_load\_fail\_time”: String?, “secondary\_product\_id”: String, “feature\_gating”: Int, “paywall\_response\_load\_complete\_time”: String?, “is\_free\_trial\_available”: Bool, “is\_superwall”: true, “presented\_by”: String, “paywall\_name”: String, “paywall\_response\_load\_duration”: String?, “paywall\_identifier”: String, “paywall\_webview\_load\_start\_time”: String?, “paywall\_webview\_load\_duration”: String?, “paywall\_products\_load\_complete\_time”: String?, “paywall\_products\_load\_duration”: String?, “paywall\_product\_ids”: String, “tertiary\_product\_id”: String, “paywall\_id”: String, “app\_session\_id”: String, “paywall\_products\_load\_start\_time”: String?, “primary\_product\_id”: String, “survey\_attached”: Bool, “survey\_presentation”: String?, “presentation\_id”: String?, “close\_reason”: String, “cache\_key”: String, “build\_id”: String, “is\_scroll\_enabled”: Bool, “intro\_offer\_eligibility”: String, “shimmerView\_load\_start\_time”: String, “shimmerView\_load\_complete\_time”: String] | | [`paywall_decline`](/docs/dashboard/dashboard-campaigns/campaigns-standard-placements#using-the-paywall-decline-event) | When a user manually dismisses a paywall. | Same as `paywall_close` | | `paywall_open` | When a paywall is opened. | Same as `paywall_close` | | `paywall_page_view` | When a user views a page in a multi-page paywall (including the first page). | Same as `paywall_close` + `["page_node_id": String, "flow_position": Int, "page_name": String, "navigation_node_id": String, "navigation_type": String, "previous_page_node_id": String?, "previous_flow_position": Int?, "time_on_previous_page_ms": Int?]` | | `paywallPresentationRequest` | When something happened during the paywall presentation, whether a success or failure. | `[“source_event_name”: String, “status”: String, “is_superwall”: true, “app_session_id”: String, “pipeline_type”: String, “status_reason”: String]` | | `paywallPreload_complete` | When paywall preloading completes. | `["paywall_count": Int]` | | `paywallPreload_start` | When paywall preloading starts. | `["paywall_count": Int]` | | `paywallProductsLoad_complete` | When the request to load a paywall's products completes. | Same as `paywallResponseLoad_start` | | `paywallProductsLoad_fail` | When the request to load a paywall's products fails. | Same as `paywallResponseLoad_start` | | `paywallProductsLoad_missingProducts` | When some of the paywall's products are missing from the store. | Same as `paywallResponseLoad_start` | | `paywallProductsLoad_retry` | When the request to load a paywall's products fails and is being retried. | `["triggeredPlacementName": String?, "paywallInfo": PaywallInfo, "attempt": Int]` | | `paywallProductsLoad_start` | When the request to load a paywall's products starts. | Same as `paywallResponseLoad_start` | | `paywallResourceLoad_fail` | When a resource on the paywall's webpage fails to load (Android only). | `["url": String, "error": String]` | | `paywallResponseLoad_complete` | When a paywall request to Superwall's servers completes. | Same as `paywallResponseLoad_start` | | `paywallResponseLoad_fail` | When a paywall request to Superwall's servers fails. | Same as `paywallResponseLoad_start` | | `paywallResponseLoad_notFound` | When a paywall request returns a 404 error. | Same as `paywallResponseLoad_start` | | `paywallResponseLoad_start` | When a paywall request to Superwall's servers has started. | Same as `app_install` + `["is_triggered_from_event": Bool]` | | `paywallWebviewLoad_complete` | When a paywall's webpage completes loading. | Same as `paywall_close` | | `paywallWebviewLoad_fail` | When a paywall's webpage fails to load. | Same as `paywall_close` | | `paywallWebviewLoad_fallback` | When a paywall's webpage fails and loads a fallback version. | Same as `paywall_close` | | `paywallWebviewLoad_start` | When a paywall's webpage begins to load. | Same as `paywall_close` | | `paywallWebviewLoad_timeout` | When the loading of a paywall's webpage times out. | Same as `paywall_close` | | `paywallWebviewLoad_processTerminated` | When the paywall's web view content process terminates. | Same as `paywall_close` | | `permission_denied` | When a permission is denied after being requested from a paywall. | `["permission_name": String, "paywall_identifier": String]` | | `permission_granted` | When a permission is granted after being requested from a paywall. | `["permission_name": String, "paywall_identifier": String]` | | `permission_requested` | When a permission is requested from a paywall. | `["permission_name": String, "paywall_identifier": String]` | | `redemption_complete` | When the redemption of a code completes. | `["type": String, "code": String?]` | | `redemption_fail` | When the redemption of a code fails. | `["type": String, "code": String?]` | | `redemption_start` | When the redemption of a code is initiated. | `["type": String, "code": String?]` | | `reset` | When `Superwall.reset()` is called. | None | | `restore_complete` | When a restore completes successfully. | Same as `paywall_close` | | `restore_fail` | When a restore fails. | Same as `paywall_close` + `["error_message": String]` | | `restore_start` | When a restore is initiated. | Same as `paywall_close` | | `review_requested` | When a review is requested from the user. | `["count": Int, "type": String]` | | `session_start` | When the app is opened after at least 60 minutes since last `app_close`. | Same as `app_install` | | `shimmerView_complete` | When the shimmer view stops showing. | `["paywall_id": String, "visible_duration": Double]` | | `shimmerView_start` | When the shimmer view starts showing. | `["paywall_id": String]` | | `stripeCheckout_complete` | When a Stripe checkout session completes. | Same as `paywall_close` + `["store": "STRIPE", "product_identifier": String, "is_triggered_from_event": Bool]` | | `stripeCheckout_fail` | When a Stripe checkout session fails. | Same as `paywall_close` + `["store": "STRIPE", "product_identifier": String, "is_triggered_from_event": Bool]` | | `stripeCheckout_start` | When a Stripe checkout session starts. | Same as `paywall_close` + `["store": "STRIPE", "product_identifier": String, "is_triggered_from_event": Bool]` | | `stripeCheckout_submit` | When a Stripe checkout form is submitted. | Same as `paywall_close` + `["store": "STRIPE", "product_identifier": String, "is_triggered_from_event": Bool]` | | `subscription_start` | When a user completes a transaction for a subscription product without an introductory offer. | \[“product\_period\_days”: String, “product\_price”: String, “presentation\_source\_type”: String?, “paywall\_response\_load\_complete\_time”: String?, “product\_language\_code”: String, “product\_trial\_period\_monthly\_price”: String, “paywall\_products\_load\_duration”: String?, “product\_currency\_symbol”: String, “is\_superwall”: true, “app\_session\_id”: String, “product\_period\_months”: String, “presented\_by\_event\_id”: String?, “product\_id”: String, “trigger\_session\_id”: String, “paywall\_webview\_load\_complete\_time”: String?, “paywall\_response\_load\_start\_time”: String?, “product\_raw\_trial\_period\_price”: String, “feature\_gating”: Int, “paywall\_id”: String, “product\_trial\_period\_daily\_price”: String, “product\_period\_years”: String, “presented\_by”: String, “product\_period”: String, “paywall\_url”: String, “paywall\_name”: String, “paywall\_identifier”: String, “paywall\_products\_load\_start\_time”: String?, “product\_trial\_period\_months”: String, “product\_currency\_code”: String, “product\_period\_weeks”: String, “product\_periodly”: String, “product\_trial\_period\_text”: String, “paywall\_webview\_load\_start\_time”: String?, “paywall\_products\_load\_complete\_time”: String?, “primary\_product\_id”: String, “product\_trial\_period\_yearly\_price”: String, “paywalljs\_version”: String?, “product\_trial\_period\_years”: String, “tertiary\_product\_id”: String, “paywall\_products\_load\_fail\_time”: String?, “product\_trial\_period\_end\_date”: String, “product\_weekly\_price”: String, “variant\_id”: String, “presented\_by\_event\_timestamp”: String?, “paywall\_response\_load\_duration”: String?, “secondary\_product\_id”: String, “product\_trial\_period\_days”: String, “product\_monthly\_price”: String, “paywall\_product\_ids”: String, “product\_locale”: String, “product\_daily\_price”: String, “product\_raw\_price”: String, “product\_yearly\_price”: String, “product\_trial\_period\_price”: String, “product\_localized\_period”: String, “product\_identifier”: String, “experiment\_id”: String, “is\_free\_trial\_available”: Bool, “product\_trial\_period\_weeks”: String, “paywall\_webview\_load\_duration”: String?, “product\_period\_alt”: String, “product\_trial\_period\_weekly\_price”: String, “presented\_by\_event\_name”: String?] | | `subscriptionStatus_didChange` | When a user's subscription status changes. | `["is_superwall": true, "app_session_id": String, "subscription_status": String]` | | `survey_close` | When the user chooses to close a survey instead of responding. | None | | [`survey_response`](/docs/dashboard/dashboard-campaigns/campaigns-standard-placements#using-the-survey-response-event) | When a user responds to a paywall survey. | `["survey_selected_option_title": String, "survey_custom_response": String, "survey_id": String, "survey_assignment_key": String, "survey_selected_option_id": String]` | | `testModeModal_open` | When the test mode modal is opened. | None | | `testModeModal_close` | When the test mode modal is closed. | `["free_trial_override": String]` + `entitlement__state` and `entitlement__offer_type` for each entitlement | | `touches_began` | When the user touches the app's UIWindow for the first time (if tracked by a campaign). | Same as `app_install` | | `transaction_abandon` | When the user cancels a transaction. | Same as `subscription_start` | | `transaction_complete` | When the user completes checkout and any product is purchased. | Same as subscription\_start + \[“web\_order\_line\_item\_id”: String, “app\_bundle\_id”: String, “config\_request\_id”: String, “state”: String, “subscription\_group\_id”: String, “is\_upgraded”: String, “expiration\_date”: String, “trigger\_session\_id”: String, “original\_transaction\_identifier”: String, “id”: String, “transaction\_date”: String, “is\_superwall”: true, “store\_transaction\_id”: String, “original\_transaction\_date”: String, “app\_session\_id”: String] | | `transaction_fail` | When the payment sheet fails to complete a transaction (ignores user cancellation). | Same as `subscription_start` + `["message": String]` | | `transaction_restore` | When the user successfully restores their purchases. | Same as `subscription_start` | | `transaction_start` | When the payment sheet is displayed to the user. | Same as `subscription_start` | | `transaction_timeout` | When the transaction takes longer than 5 seconds to display the payment sheet. | `["paywallInfo": PaywallInfo]` | | `trigger_fire` | When a registered placement triggers a paywall. | `[“trigger_name”: String, “trigger_session_id”: String, “variant_id”: String?, “experiment_id”: String?, “paywall_identifier”: String?, “result”: String, “unmatched_rule_”: “”]. unmatched_rule_ indicates why a rule (with a specfiic experiment id) didn’t match. It will only exist if the result is no_rule_match. Its outcome will either be OCCURRENCE, referring to the limit applied to a rule, or EXPRESSION.` | | `user_attributes` | When the user attributes are set. | `[“aliasId”: String, “seed”: Int, “app_session_id”: String, “applicationInstalledAt”: String, “is_superwall”: true, “application_installed_at”: String] + provided attributes` | # 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 # Advanced Purchasing If you need fine-grain control over the purchasing pipeline, use a purchase controller to manually handle purchases and subscription status. > **Warning:** Using a `PurchaseController` is only recommended for **advanced** use cases. By default, Superwall handles all > subscription-related logic and purchasing operations for you out of the box. By default, Superwall handles basic subscription-related logic for you: 1. **Purchasing**: When the user initiates a checkout on a paywall. 2. **Restoring**: When the user restores previously purchased products. 3. **Subscription Status**: When the user's subscription status changes to active or expired (by checking the local receipt). However, if you want more control, you can pass in a `PurchaseController` when configuring the SDK via `configure(apiKey:purchaseController:options:)` and manually set `Superwall.shared.subscriptionStatus` to take over this responsibility. On iOS, starting in `4.15.0`, a `PurchaseController` is also how you handle custom products attached to Superwall paywalls. Those products are not purchased with StoreKit, so your controller must route them through your own billing system. See [Custom Store Products](/docs/ios/guides/custom-store-products) for the full iOS setup. ### Step 1: Creating a `PurchaseController` A `PurchaseController` handles purchasing and restoring via protocol methods that you implement. :::flutter ```dart Flutter // MyPurchaseController.dart class MyPurchaseController extends PurchaseController { // 1 @override Future purchaseFromAppStore(String productId) async { // TODO // ---- // Purchase via StoreKit, RevenueCat, Qonversion or however // you like and return a valid PurchaseResult return PurchaseResult.purchased; } @override Future purchaseFromGooglePlay( String productId, String? basePlanId, String? offerId ) async { // TODO // ---- // Purchase via Google Billing, RevenueCat, Qonversion or however // you like and return a valid PurchaseResult return PurchaseResult.purchased; } // 2 @override Future restorePurchases() async { // TODO // ---- // Restore purchases and return true if successful. return RestorationResult.restored; } } ``` ::: Here’s what each method is responsible for: 1. Purchasing a given product. In here, enter your code that you use to purchase a product. Then, return the result of the purchase as a `PurchaseResult`. For Flutter, this is separated into purchasing from the App Store and Google Play. This is an enum that contains the following cases, all of which must be handled: 1. `.cancelled`: The purchase was cancelled. 2. `.purchased`: The product was purchased. 3. `.pending`: The purchase is pending/deferred and requires action from the developer. 4. `.failed(Error)`: The purchase failed for a reason other than the user cancelling or the payment pending. 2. Restoring purchases. Here, you restore purchases and return a `RestorationResult` indicating whether the restoration was successful or not. If it was, return `.restore`, or `failed` along with the error reason. ### Step 2: Configuring the SDK With Your `PurchaseController` Pass your purchase controller to the `configure(apiKey:purchaseController:options:)` method: :::flutter ```dart Flutter // main.dart void initState() { // Determine Superwall API Key for platform String apiKey = Platform.isIOS ? "MY_IOS_API_KEY" : "MY_ANDROID_API_KEY"; // Create the purchase controller MyPurchaseController purchaseController = MyPurchaseController(); Superwall.configure(apiKey, purchaseController); } ``` ::: ### Step 3: Keeping `subscriptionStatus` Up-To-Date You **must** set `Superwall.shared.subscriptionStatus` every time the user's subscription status changes, otherwise the SDK won't know who to show a paywall to. This is an enum that has three possible cases: 1. **`.unknown`**: This is the default value. In this state, paywalls will not show and their presentation will be ***automatically delayed*** until `subscriptionStatus` changes to a different value. 2. **`.active(let entitlements)`**: Indicates that the user has an active entitlement. Paywalls will not show in this state unless you remotely set the paywall to ignore subscription status. A user can have one or more active entitlement. 3. **`.inactive`**: Indicates that the user doesn't have an active entitlement. Paywalls can show in this state. Here's how you might do this: :::flutter ```dart Flutter // When a subscription is purchased, restored, validated, expired, etc... myService.addSubscriptionStatusListener((subscriptionInfo) { var entitlements = subscriptionInfo.entitlements.active.keys .map((id) => Entitlement(id: id)) .toSet(); var hasActiveSubscription = subscriptionInfo.isActive; if (hasActiveSubscription) { Superwall.shared.setSubscriptionStatus(SubscriptionStatusActive(entitlements: entitlements)); } else { Superwall.shared.setSubscriptionStatus(SubscriptionStatusInactive()); } }); ``` :::
> **Note:** `subscriptionStatus` is cached between app launches ### Listening for subscription status changes If you need a simple way to observe when a user's subscription status changes, on iOS you can use the `Publisher` for it. Here's an example: :::flutter ```dart Flutter Superwall.shared.subscriptionStatus.listen((status) { // React to changes } //Or use SuperwallBuilder widget which triggers the builder closure when subscription status changes SuperwallBuilder( builder: (context, status) => Center( child: Text('Subscription Status: ${status}'), ) ) ``` ::: You can do similar tasks with the `SuperwallDelegate`, such as [viewing which product was purchased from a paywall](/docs/sdk/guides/3rd-party-analytics#using-events-to-see-purchased-products). ### Product Overrides > **Note:** Product overrides allow you to dynamically substitute products on paywalls without modifying the paywall design in the Superwall dashboard. When using a `PurchaseController`, you may want to override specific products shown on your paywalls. This is useful for: * A/B testing different subscription tiers * Showing region-specific products * Dynamically changing products based on user segments * Testing promotional pricing without modifying paywalls **How Product Overrides Work:** 1. Product names (e.g., "primary", "secondary") must match exactly as defined in the Superwall dashboard's Paywall Editor 2. The SDK substitutes the original product IDs with your override IDs before fetching from the App Store 3. The paywall maintains its visual design while showing the substituted products 4. Your `PurchaseController` will receive the overridden products when `purchase(product:)` is called > **Warning:** Product overrides only affect the products shown on paywalls. They don't change your subscription logic or entitlement validation. # 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 # Custom Paywall Actions For example, adding a custom action called `help_center` to a button in your paywall gives you the opportunity to present a help center whenever that button is pressed. To set this up, implement `handleCustomPaywallAction(withName:)` in your `SuperwallDelegate`: :::flutter ```dart @override void handleCustomPaywallAction(String name) { if (name == "help_center") { HelpCenterManager.present(); } } ``` :::
> **Note:** Remember to set `Superwall.shared.delegate`! For implementation details, see the [Superwall Delegate](/docs/sdk/guides/using-superwall-delegate) guide. # 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 # Game Controller Support :::android First set the `SuperwallOption` `isGameControllerEnabled` to `true`: ```kotlin Superwall.instance.options.isGameControllerEnabled = true ``` Then Superwall will automatically listen for gamepad events and forward them to your paywall! ::: :::flutter First set the `SuperwallOption` `isGameControllerEnabled` to `true`: ```dart Superwall.instance.options.isGameControllerEnabled = true ``` Then Superwall will automatically listen for gamepad events and forward them to your paywall! ::: :::expo First set the `SuperwallOption` `isGameControllerEnabled` to `true`: ```typescript Superwall.instance.options.isGameControllerEnabled = true ``` Then Superwall will automatically listen for gamepad events and forward them to your paywall! ::: # 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 # Observer Mode If you wish to make purchases outside of Superwall's SDK and paywalls, you can use **observer mode** to report purchases that will appear in the Superwall dashboard, such as transactions: ![](/docs/images/om_transactions.png) This is useful if you are using Superwall solely for revenue tracking, and you're making purchases using frameworks like StoreKit or Google Play Billing Library directly. Observer mode will also properly link user identifiers to transactions. To enable observer mode, set it using `SuperwallOptions` when configuring the SDK: There are a few things to keep in mind when using observer mode: 1. On iOS, if you're using StoreKit 2, then Superwall solely reports transaction completions. If you're using StoreKit 1, then Superwall will report transaction starts, abandons, and completions. 2. When using observer mode, you can't make purchases using our SDK — such as `Superwall.shared.purchase(aProduct)`. For more on setting up revenue tracking, check out this [doc](/docs/dashboard/dashboard-settings/overview-settings-revenue-tracking). # 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 # Retrieving and Presenting a Paywall Yourself If you want complete control over the paywall presentation process, you can use `getPaywall(forPlacement:params:paywallOverrides:delegate:)`. This returns the `UIViewController` subclass `PaywallViewController`, which you can then present however you like. Or, you can use a SwiftUI `View` via `PaywallView`. The following is code is how you'd mimic [register](/docs/sdk/quickstart/feature-gating): ## Tab ```swift Swift final class MyViewController: UIViewController { private func presentPaywall() async { do { // 1 let paywallVc = try await Superwall.shared.getPaywall( forPlacement: "campaign_trigger", delegate: self ) self.present(paywallVc, animated: true) } catch let skippedReason as PaywallSkippedReason { // 2 switch skippedReason { case .holdout, .noAudienceMatch, .placementNotFound: break } } catch { // 3 print(error) } } private func launchFeature() { // Insert code to launch a feature that's behind your paywall. } } // 4 extension MyViewController: PaywallViewControllerDelegate { func paywall( _ paywall: PaywallViewController, didFinishWith result: PaywallResult, shouldDismiss: Bool ) { if shouldDismiss { paywall.dismiss(animated: true) } switch result { case .purchased, .restored: launchFeature() case .declined: let closeReason = paywall.info.closeReason let featureGating = paywall.info.featureGatingBehavior if closeReason != .forNextPaywall && featureGating == .nonGated { launchFeature() } } } } ``` ## Tab ```swift Objective-C @interface MyViewController : UIViewController - (void)presentPaywall; @end @interface MyViewController () @end @implementation MyViewController - (void)presentPaywall { // 1 [[Superwall sharedInstance] getPaywallForEvent:@"campaign_trigger" params:nil paywallOverrides:nil delegate:self completion:^(SWKGetPaywallResult * _Nonnull result) { if (result.paywall != nil) { [self presentViewController:result.paywall animated:YES completion:nil]; } else if (result.skippedReason != SWKPaywallSkippedReasonNone) { switch (result.skippedReason) { // 2 case SWKPaywallSkippedReasonHoldout: case SWKPaywallSkippedReasonUserIsSubscribed: case SWKPaywallSkippedReasonEventNotFound: case SWKPaywallSkippedReasonNoRuleMatch: case SWKPaywallSkippedReasonNone: break; }; } else if (result.error) { // 3 NSLog(@"%@", result.error); } }]; } -(void)launchFeature { // Insert code to launch a feature that's behind your paywall. } // 4 - (void)paywall:(SWKPaywallViewController *)paywall didFinishWithResult:(enum SWKPaywallResult)result shouldDismiss:(BOOL)shouldDismiss { if (shouldDismiss) { [paywall dismissViewControllerAnimated:true completion:nil]; } SWKPaywallCloseReason closeReason; SWKFeatureGatingBehavior featureGating; switch (result) { case SWKPaywallResultPurchased: case SWKPaywallResultRestored: [self launchFeature]; break; case SWKPaywallResultDeclined: closeReason = paywall.info.closeReason; featureGating = paywall.info.featureGatingBehavior; if (closeReason != SWKPaywallCloseReasonForNextPaywall && featureGating == SWKFeatureGatingBehaviorNonGated) { [self launchFeature]; } break; } } @end ``` ## Tab ```swift SwiftUI import SuperwallKit struct MyAwesomeApp: App { @State var store: AppStore = .init() init() { Superwall.configure(apiKey: "MyAPIKey") } var body: some Scene { WindowGroup { ContentView() .fullScreenCover(isPresented: $store.showPaywall) { // You can just use 'placement' at a minimum. The 'feature' // Closure fires if they convert PaywallView(placement: "a_placement", onSkippedView: { skip in switch skip { case .userIsSubscribed, .holdout(_), .noRuleMatch, .eventNotFound: MySkipView() } }, onErrorView: { error in MyErrorView() }, feature: { // User is subscribed as a result of the paywall purchase // Or they already were (which would happen in `onSkippedView`) }) } } } } ``` ## Tab ```kotlin Kotlin // This is an example of how to use `getPaywall` to use a composable` import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.viewinterop.AndroidView import com.superwall.sdk.Superwall import com.superwall.sdk.paywall.presentation.get_paywall.getPaywall import com.superwall.sdk.paywall.presentation.internal.request.PaywallOverrides import com.superwall.sdk.paywall.vc.PaywallView import com.superwall.sdk.paywall.vc.delegate.PaywallViewCallback @Composable fun PaywallComposable( event: String, params: Map? = null, paywallOverrides: PaywallOverrides? = null, callback: PaywallViewCallback, errorComposable: @Composable ((Throwable) -> Unit) = { error: Throwable -> // Default error composable Text(text = "No paywall to display") }, loadingComposable: @Composable (() -> Unit) = { // Default loading composable Box(modifier = Modifier.fillMaxSize()) { Column( modifier = Modifier.align(Alignment.Center), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { CircularProgressIndicator() } } } ) { val viewState = remember { mutableStateOf(null) } val errorState = remember { mutableStateOf(null) } val context = LocalContext.current LaunchedEffect(Unit) { PaywallBuilder(event) .params(params) .overrides(paywallOverrides) .delegate(delegate) .activity(context as Activity) .build() .fold(onSuccess = { viewState.value = it }, onFailure = { errorState.value = it }) } when { viewState.value != null -> { viewState.value?.let { viewToRender -> DisposableEffect(viewToRender) { viewToRender.onViewCreated() onDispose { viewToRender.beforeOnDestroy() viewToRender.encapsulatingActivity = null CoroutineScope(Dispatchers.Main).launch { viewToRender.destroyed() } } } AndroidView( factory = { context -> viewToRender } ) } } errorState.value != null -> { errorComposable(errorState.value!!) } else -> { loadingComposable() } } } ``` This does the following: 1. Gets the paywall view controller. 2. Handles the cases where the paywall was skipped. 3. Catches any presentation errors. 4. Implements the delegate. This is called when the user is finished with the paywall. First, it checks `shouldDismiss`. If this is true then is dismissed the paywall from view before launching any features. This may depend on the `result` depending on how you first presented your view. Then, it switches over the `result`. If the result is `purchased` or `restored` the feature can be launched. However, if the result is `declined`, it checks that the the `featureGating` property of `paywall.info` is `nonGated` and that the `closeReason` isn't `.forNextPaywall`. ### Best practices 1. **Make sure to prevent a paywall from being accessed after a purchase has occurred**. If a user purchases from a paywall, it is your responsibility to make sure that the user can't access that paywall again. For example, if after successful purchase you decide to push a new view on to the navigation stack, you should make sure that the user can't go back to access the paywall. 2. **Make sure the paywall view controller deallocates before presenting it elsewhere**. If you have a paywall view controller presented somewhere and you try to present the same view controller elsewhere, you will get a crash. For example, you may have a paywall in a tab bar controller, and then you also try to present it modally. We plan on improving this, but currently it's your responsibility to ensure this doesn't happen. # 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 the Presentation Handler You can provide a `PaywallPresentationHandler` to `register`, whose functions provide status updates for a paywall: * `onDismiss`: Called when the paywall is dismissed. Accepts a `PaywallInfo` object containing info about the dismissed paywall, and there is a `PaywallResult` informing you of any transaction. * `onPresent`: Called when the paywall did present. Accepts a `PaywallInfo` object containing info about the presented paywall. * `onError`: Called when an error occurred when trying to present a paywall. Accepts an `Error` indicating why the paywall could not present. * `onSkip`: Called when a paywall is skipped. Accepts a `PaywallSkippedReason` enum indicating why the paywall was skipped. * `onCustomCallback` *(Android 2.7.0+)*: Called when the paywall requests a custom callback. Accepts a `CustomCallback` containing the callback name and optional variables, and returns a `CustomCallbackResult` indicating success or failure with optional data to pass back to the paywall. ## Tab ```swift Swift let handler = PaywallPresentationHandler() handler.onDismiss { paywallInfo, result in print("The paywall dismissed. PaywallInfo: \(paywallInfo). Result: \(result)") } handler.onPresent { paywallInfo in print("The paywall presented. PaywallInfo:", paywallInfo) } handler.onError { error in print("The paywall presentation failed with error \(error)") } handler.onSkip { reason in switch reason { case .holdout(let experiment): print("Paywall not shown because user is in a holdout group in Experiment: \(experiment.id)") case .noAudienceMatch: print("Paywall not shown because user doesn't match any audiences.") case .placementNotFound: print("Paywall not shown because this placement isn't part of a campaign.") } } Superwall.shared.register(placement: "campaign_trigger", handler: handler) { // Feature launched } ``` ## Tab ```swift Objective-C SWKPaywallPresentationHandler *handler = [[SWKPaywallPresentationHandler alloc] init]; [handler onDismiss:^(SWKPaywallInfo * _Nonnull paywallInfo, enum SWKPaywallResult result, SWKStoreProduct * _Nullable product) { NSLog(@"The paywall presented. PaywallInfo: %@ - result: %ld", paywallInfo, (long)result); }]; [handler onPresent:^(SWKPaywallInfo * _Nonnull paywallInfo) { NSLog(@"The paywall presented. PaywallInfo: %@", paywallInfo); }]; [handler onError:^(NSError * _Nonnull error) { NSLog(@"The paywall presentation failed with error %@", error); }]; [handler onSkip:^(enum SWKPaywallSkippedReason reason) { switch (reason) { case SWKPaywallSkippedReasonUserIsSubscribed: NSLog(@"Paywall not shown because user is subscribed."); break; case SWKPaywallSkippedReasonHoldout: NSLog(@"Paywall not shown because user is in a holdout group."); break; case SWKPaywallSkippedReasonNoAudienceMatch: NSLog(@"Paywall not shown because user doesn't match any audiences."); break; case SWKPaywallSkippedReasonPlacementNotFound: NSLog(@"Paywall not shown because this placement isn't part of a campaign."); break; case SWKPaywallSkippedReasonNone: // The paywall wasn't skipped. break; } }]; [[Superwall sharedInstance] registerWithPlacement:@"campaign_trigger" params:nil handler:handler feature:^{ // Feature launched. }]; ``` ## Tab ```kotlin Kotlin val handler = PaywallPresentationHandler() handler.onDismiss { paywallInfo, result -> println("The paywall dismissed. PaywallInfo: ${it}") } handler.onPresent { println("The paywall presented. PaywallInfo: ${it}") } handler.onError { println("The paywall errored. Error: ${it}") } handler.onSkip { when (it) { is PaywallSkippedReason.PlacementNotFound -> { println("The paywall was skipped because the placement was not found.") } is PaywallSkippedReason.Holdout -> { println("The paywall was skipped because the user is in a holdout group.") } is PaywallSkippedReason.NoAudienceMatch -> { println("The paywall was skipped because no audience matched.") } } } Superwall.instance.register(placement = "campaign_trigger", handler = handler) { // Feature launched } ``` ## Tab ```dart Flutter PaywallPresentationHandler handler = PaywallPresentationHandler(); handler.onPresent((paywallInfo) async { String name = await paywallInfo.name; print("Handler (onPresent): $name"); }); handler.onDismiss((paywallInfo, paywallResult) async { String name = await paywallInfo.name; print("Handler (onDismiss): $name"); }); handler.onError((error) { print("Handler (onError): ${error}"); }); handler.onSkip((skipReason) async { String description = await skipReason.description; if (skipReason is PaywallSkippedReasonHoldout) { print("Handler (onSkip): $description"); final experiment = await skipReason.experiment; final experimentId = await experiment.id; print("Holdout with experiment: ${experimentId}"); } else if (skipReason is PaywallSkippedReasonNoAudienceMatch) { print("Handler (onSkip): $description"); } else if (skipReason is PaywallSkippedReasonPlacementNotFound) { print("Handler (onSkip): $description"); } else { print("Handler (onSkip): Unknown skip reason"); } }); Superwall.shared.registerPlacement("campaign_trigger", handler: handler, feature: () { // Feature launched }); ``` ## Tab ```typescript React Native const handler = new PaywallPresentationHandler() handler.onPresent((paywallInfo) => { const name = paywallInfo.name console.log(`Handler (onPresent): ${name}`) }) handler.onDismiss((paywallInfo, paywallResult) => { const name = paywallInfo.name console.log(`Handler (onDismiss): ${name}`) }) handler.onError((error) => { console.log(`Handler (onError): ${error}`) }) handler.onSkip((skipReason) => { const description = skipReason.description if (skipReason instanceof PaywallSkippedReasonHoldout) { console.log(`Handler (onSkip): ${description}`) const experiment = skipReason.experiment const experimentId = experiment.id console.log(`Holdout with experiment: ${experimentId}`) } else if (skipReason instanceof PaywallSkippedReasonNoAudienceMatch) { console.log(`Handler (onSkip): ${description}`) } else if (skipReason instanceof PaywallSkippedReasonPlacementNotFound) { console.log(`Handler (onSkip): ${description}`) } else { console.log(`Handler (onSkip): Unknown skip reason`) } }) Superwall.shared.register({ placement: 'campaign_trigger', handler: handler, feature: () => { // Feature launched } }); ``` > **Tip:** Wanting to see which product was just purchased from a paywall? Use `onDismiss` and the `result` > parameter. Or, you can use the > [SuperwallDelegate](/docs/sdk/guides/3rd-party-analytics#using-events-to-see-purchased-products). # 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 # Viewing Purchased Products When a paywall is presenting and a user converts, you can view the purchased products in several different ways. ### Use the `PaywallPresentationHandler` Arguably the easiest of the options — simply pass in a presentation handler and check out the product within the `onDismiss` block. ## Tab ```swift Swift let handler = PaywallPresentationHandler() handler.onDismiss { _, result in switch result { case .declined: print("No purchased occurred.") case .purchased(let product): print("Purchased \(product.productIdentifier)") case .restored: print("Restored purchases.") } } Superwall.shared.register(placement: "caffeineLogged", handler: handler) { logCaffeine() } ``` ## Tab ```swift Objective-C SWKPaywallPresentationHandler *handler = [SWKPaywallPresentationHandler new]; [handler onDismiss:^(SWKPaywallInfo * _Nonnull info, enum SWKPaywallResult result, SWKStoreProduct * _Nullable product) { switch (result) { case SWKPaywallResultPurchased: NSLog(@"Purchased %@", product.productIdentifier); default: NSLog(@"Unhandled event."); } }]; [[Superwall sharedInstance] registerWithPlacement:@"caffeineLogged" params:@{} handler:handler feature:^{ [self logCaffeine]; }]; ``` ## Tab ```kotlin Android val handler = PaywallPresentationHandler() handler.onDismiss { _, paywallResult -> when (paywallResult) { is PaywallResult.Purchased -> { // The user made a purchase! val purchasedProductId = paywallResult.productId println("User purchased product: $purchasedProductId") // ... do something with the purchased product ID ... } is PaywallResult.Declined -> { // The user declined to make a purchase. println("User declined to make a purchase.") // ... handle the declined case ... } is PaywallResult.Restored -> { // The user restored a purchase. println("User restored a purchase.") // ... handle the restored case ... } } } Superwall.instance.register(placement = "caffeineLogged", handler = handler) { logCaffeine() } ``` ## Tab ```dart Flutter PaywallPresentationHandler handler = PaywallPresentationHandler(); handler.onDismiss((paywallInfo, paywallResult) async { String name = await paywallInfo.name; print("Handler (onDismiss): $name"); switch (paywallResult) { case PurchasedPaywallResult(productId: var id): // The user made a purchase! print('User purchased product: $id'); // ... do something with the purchased product ID ... break; case DeclinedPaywallResult(): // The user declined to make a purchase. print('User declined the paywall.'); // ... handle the declined case ... break; case RestoredPaywallResult(): // The user restored a purchase. print('User restored a previous purchase.'); // ... handle the restored case ... break; } }); Superwall.shared.registerPlacement( "caffeineLogged", handler: handler, feature: () { logCaffeine(); }); ``` ## Tab ```typescript React Native import * as React from "react" import Superwall from "../../src" import { PaywallPresentationHandler, PaywallInfo } from "../../src" import type { PaywallResult } from "../../src/public/PaywallResult" const Home = () => { const navigation = useNavigation() const presentationHandler: PaywallPresentationHandler = { onDismiss: (handler: (info: PaywallInfo, result: PaywallResult) => void) => { handler = (info, result) => { console.log("Paywall dismissed with info:", info, "and result:", result) if (result.type === "purchased") { console.log("Product purchased with ID:", result.productId) } } }, onPresent: (handler: (info: PaywallInfo) => void) => { handler = (info) => { console.log("Paywall presented with info:", info) // Add logic for when the paywall is presented } }, onError: (handler: (error: string) => void) => { handler = (error) => { console.error("Error presenting paywall:", error) // Handle any errors that occur during presentation } }, onSkip: () => { console.log("Paywall presentation skipped") // Handle the case where the paywall presentation is skipped }, } const nonGated = () => { Superwall.shared.register({ placement: "non_gated", handler: presentationHandler, feature: () => { navigation.navigate("caffeineLogged", { value: "Go for caffeine logging", }) }); } return // Your view code here } ``` ### Use `SuperwallDelegate` Next, the [SuperwallDelegate](/docs/sdk/guides/using-superwall-delegate) offers up much more information, and can inform you of virtually any Superwall event that occurred: ## Tab ```swift Swift class SWDelegate: SuperwallDelegate { func handleSuperwallEvent(withInfo eventInfo: SuperwallEventInfo) { switch eventInfo.event { case .transactionComplete(_, let product, _, _): print("Transaction complete: product: \(product.productIdentifier)") case .subscriptionStart(let product, _): print("Subscription start: product: \(product.productIdentifier)") case .freeTrialStart(let product, _): print("Free trial start: product: \(product.productIdentifier)") case .transactionRestore(_, _): print("Transaction restored") case .nonRecurringProductPurchase(let product, _): print("Consumable product purchased: \(product.id)") default: print("Unhandled event.") } } } @main struct Caffeine_PalApp: App { @State private var swDelegate: SWDelegate = .init() init() { Superwall.configure(apiKey: "my_api_key") Superwall.shared.delegate = swDelegate } var body: some Scene { WindowGroup { ContentView() } } } ``` ## Tab ```swift Objective-C // SWDelegate.h... #import @import SuperwallKit; NS_ASSUME_NONNULL_BEGIN @interface SWDelegate : NSObject @end NS_ASSUME_NONNULL_END // SWDelegate.m... @implementation SWDelegate - (void)handleSuperwallEventWithInfo:(SWKSuperwallEventInfo *)eventInfo { switch(eventInfo.event) { case SWKSuperwallEventTransactionComplete: NSLog(@"Transaction complete: %@", eventInfo.params[@"primary_product_id"]); } } // In AppDelegate.m... #import "AppDelegate.h" #import "SWDelegate.h" @import SuperwallKit; @interface AppDelegate () @property (strong, nonatomic) SWDelegate *delegate; @end @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. self.delegate = [SWDelegate new]; [Superwall configureWithApiKey:@"my_api_key"]; [Superwall sharedInstance].delegate = self.delegate; return YES; } ``` ## Tab ```kotlin Android class SWDelegate : SuperwallDelegate { override fun handleSuperwallEvent(eventInfo: SuperwallEventInfo) { when (eventInfo.event) { is SuperwallPlacement.TransactionComplete -> { val transaction = (eventInfo.event as SuperwallPlacement.TransactionComplete).transaction val product = (eventInfo.event as SuperwallPlacement.TransactionComplete).product val paywallInfo = (eventInfo.event as SuperwallPlacement.TransactionComplete).paywallInfo println("Transaction Complete: $transaction, Product: $product, Paywall Info: $paywallInfo") } else -> { // Handle other cases } } } } class MyApplication : Application() { override fun onCreate() { super.onCreate() Superwall.configure(this, "my_api_key") Superwall.instance.delegate = SWDelegate() } } ``` ## Tab ```dart Flutter import 'dart:io'; import 'package:flutter/material.dart'; import 'package:superwallkit_flutter/superwallkit_flutter.dart'; class _MyAppState extends State implements SuperwallDelegate { final logging = Logging(); @override void initState() { super.initState(); configureSuperwall(useRevenueCat); } Future configureSuperwall(bool useRevenueCat) async { try { final apiKey = Platform.isIOS ? 'ios_api_project_key' : 'android_api_project_key'; final logging = Logging(); logging.level = LogLevel.warn; logging.scopes = {LogScope.all}; final options = SuperwallOptions(); options.paywalls.shouldPreload = false; options.logging = logging; Superwall.configure(apiKey, purchaseController: null, options: options, completion: () { logging.info('Executing Superwall configure completion block'); }); Superwall.shared.setDelegate(this); } catch (e) { // Handle any errors that occur during configuration logging.error('Failed to configure Superwall:', e); } } @override Future handleSuperwallEvent(SuperwallEventInfo eventInfo) async { switch (eventInfo.event.type) { case PlacementType.transactionComplete: final product = eventInfo.params?['product']; logging.info('Transaction complete event received with product: $product'); // Add any additional logic you need to handle the transaction complete event break; // Handle other events if necessary default: logging.info('Unhandled event type: ${eventInfo.event.type}'); break; } } } ``` ## Tab ```typescript React Native import { PaywallInfo, SubscriptionStatus, SuperwallDelegate, SuperwallPlacementInfo, PlacementType, } from '../../src'; export class MySuperwallDelegate extends SuperwallDelegate { handleSuperwallPlacement(placementInfo: SuperwallPlacementInfo) { console.log('Handling Superwall placement:', placementInfo); switch (placementInfo.placement.type) { case PlacementType.transactionComplete: const product = placementInfo.params?.["product"]; if (product) { console.log(`Product: ${product}`); } else { console.log("Product not found in params."); } break; default: break; } } } export default function App() { const delegate = new MySuperwallDelegate(); React.useEffect(() => { const setupSuperwall = async () => { const apiKey = Platform.OS === 'ios' ? 'ios_api_project_key' : 'android_api_project_key'; Superwall.configure({ apiKey: apiKey, }); Superwall.shared.setDelegate(delegate); }; } } ``` ### Use a purchase controller If you are controlling the purchasing pipeline yourself via a [purchase controller](/docs/sdk/guides/advanced-configuration), then naturally the purchased product is available: ## Tab ```swift Swift final class MyPurchaseController: PurchaseController { func purchase(product: StoreProduct) async -> PurchaseResult { print("Kicking off purchase of \(product.productIdentifier)") do { let result = try await MyPurchaseLogic.purchase(product: product) return .purchased // .cancelled, .pending, .failed(Error) } catch { return .failed(error) } } // 2 func restorePurchases() async -> RestorationResult { print("Restoring purchases") return .restored // false } } @main struct Caffeine_PalApp: App { private let pc: MyPurchaseController = .init() init() { Superwall.configure(apiKey: "my_api_key", purchaseController: pc) } var body: some Scene { WindowGroup { ContentView() } } } ``` ## Tab ```swift Objective-C // In MyPurchaseController.h... #import @import SuperwallKit; @import StoreKit; NS_ASSUME_NONNULL_BEGIN @interface MyPurchaseController : NSObject + (instancetype)sharedInstance; @end NS_ASSUME_NONNULL_END // In MyPurchaseController.m... #import "MyPurchaseController.h" @implementation MyPurchaseController + (instancetype)sharedInstance { static MyPurchaseController *sharedInstance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstance = [MyPurchaseController new]; }); return sharedInstance; } - (void)purchaseWithProduct:(SWKStoreProduct * _Nonnull)product completion:(void (^ _Nonnull)(enum SWKPurchaseResult, NSError * _Nullable))completion { NSLog(@"Kicking off purchase of %@", product.productIdentifier); // Do purchase logic here completion(SWKPurchaseResultPurchased, nil); } - (void)restorePurchasesWithCompletion:(void (^ _Nonnull)(enum SWKRestorationResult, NSError * _Nullable))completion { // Do restore logic here completion(SWKRestorationResultRestored, nil); } @end // In AppDelegate.m... #import "AppDelegate.h" #import "MyPurchaseController.h" @import SuperwallKit; @interface AppDelegate () @end @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. [Superwall configureWithApiKey:@"my_api_key" purchaseController:[MyPurchaseController sharedInstance] options:nil completion:^{ }]; return YES; } ``` ## Tab ```kotlin Android class MyPurchaseController(val context: Context): PurchaseController { override suspend fun purchase( activity: Activity, product: StoreProduct, basePlanId: String?, offerId: String? ): PurchaseResult { if (product.isCustomProduct) { // Purchase custom products with your own billing system, then update // Superwall.instance.setSubscriptionStatus(...). return PurchaseResult.Failed("Custom product handling is not implemented") } val productDetails = product.rawStoreProduct?.underlyingProductDetails ?: return PurchaseResult.Failed("Missing Google Play product details") println("Kicking off purchase of $basePlanId") return PurchaseResult.Purchased() } override suspend fun restorePurchases(): RestorationResult { TODO("Not yet implemented") } } class MyApplication : Application() { override fun onCreate() { super.onCreate() Superwall.configure(this, "my_api_key", purchaseController = MyPurchaseController(this)) } } ``` ## Tab ```dart Flutter class MyPurchaseController extends PurchaseController { // 1 @override Future purchaseFromAppStore(String productId) async { print('Attempting to purchase product with ID: $productId'); // Do purchase logic return PurchaseResult.purchased; } @override Future purchaseFromGooglePlay( String productId, String? basePlanId, String? offerId ) async { print('Attempting to purchase product with ID: $productId and basePlanId: $basePlanId'); // Do purchase logic return PurchaseResult.purchased; } @override Future restorePurchases() async { // Do resture logic } } ``` ## Tab ```typescript React Native export class MyPurchaseController extends PurchaseController { // 1 async purchaseFromAppStore(productId: string): Promise { console.log("Kicking off purchase of ", productId) // Purchase logic return await this._purchaseStoreProduct(storeProduct) } async purchaseFromGooglePlay( productId: string, basePlanId?: string, offerId?: string ): Promise { console.log("Kicking off purchase of ", productId, " base plan ID", basePlanId) // Purchase logic return await this._purchaseStoreProduct(storeProduct) } // 2 async restorePurchases(): Promise { // TODO // ---- // Restore purchases and return true if successful. } } ``` ### SwiftUI - Use `PaywallView` The `PaywallView` allows you to show a paywall by sending it a placement. It also has a dismiss handler where the purchased product will be vended: ```swift @main struct Caffeine_PalApp: App { @State private var presentPaywall: Bool = false init() { Superwall.configure(apiKey: "my_api_key") } var body: some Scene { WindowGroup { Button("Log") { presentPaywall.toggle() } .sheet(isPresented: $presentPaywall) { PaywallView(placement: "caffeineLogged", params: nil, paywallOverrides: nil) { info, result in switch result { case .declined: print("No purchased occurred.") case .purchased(let product): print("Purchased \(product.productIdentifier)") case .restored: print("Restored purchases.") } } feature: { print("Converted") presentPaywall.toggle() } } } } } ``` # 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 # Advanced Configuration When configuring the SDK you can pass in options that configure Superwall, the paywall presentation, and its appearance. ### Logging Logging is enabled by default in the SDK and is controlled by two properties: `level` and `scopes`. `level` determines the minimum log level to print to the console. There are five types of log level: 1. **debug**: Prints all logs from the SDK to the console. Useful for debugging your app if something isn't working as expected. 2. **info**: Prints errors, warnings, and useful information from the SDK to the console. 3. **warn**: Prints errors and warnings from the SDK to the console. 4. **error**: Only prints errors from the SDK to the console. 5. **none**: Turns off all logs. The SDK defaults to `info`. `scopes` defines the scope of logs to print to the console. For example, you might only care about logs relating to `paywallPresentation` and `paywallTransactions`. This defaults to `.all`. Check out [LogScope](https://sdk.superwall.me/documentation/superwallkit/logscope) for all possible cases. You set these properties like this: :::flutter ```dart SuperwallOptions options = SuperwallOptions(); options.logging.level = LogLevel.warn; options.logging.scopes = { LogScope.paywallPresentation, LogScope.paywallEvents }; Superwall.configure( "MY_API_KEY", options: options ); // Or you can set: Superwall.logging.logLevel = LogLevel.warn; ``` ::: ### Preloading Paywalls Paywalls are preloaded by default when the app is launched from a cold start. The paywalls that are preloaded are determined by the list of placements that result in a paywall for the user when [registered](/docs/sdk/quickstart/feature-gating). Preloading is smart, only preloading paywalls that belong to audiences that could be matched. Paywalls are cached by default, which means after they load once, they don't need to be reloaded from the network unless you make a change to them on the dashboard. However, if you have a lot of paywalls, preloading may increase network usage of your app on first load of the paywalls and result in slower loading times overall. > **Tip:** To make an onboarding or first-launch paywall load before the rest of your campaigns, prioritize the campaign from the dashboard with [Priority Placements](/docs/dashboard/dashboard-campaigns/campaigns-placements-prioritized). Use the SDK methods below when you need to disable automatic preloading or manually preload specific placements. You can turn off preloading by setting `shouldPreload` to `false`: :::flutter ```dart SuperwallOptions options = SuperwallOptions(); options.paywalls.shouldPreload = false; Superwall.configure( "MY_API_KEY", options: options ); ``` ::: Then, if you'd like to preload paywalls for specific placements you can use `preloadPaywalls(forPlacements:)`: :::flutter ```dart var placements = {"campaign_trigger"}; Superwall.shared.preloadPaywallsForPlacements(placements); ``` ::: If you'd like to preload all paywalls you can use `preloadAllPaywalls()`: :::flutter ```dart Superwall.shared.preloadAllPaywalls(); ``` ::: Note: These methods will not reload any paywalls that have already been preloaded. ### Event Tracking Behavior By default, Superwall allows the SDK to send event collection that supports paywall analytics, reporting, and targeting. Use the platform-specific options below when your app needs to control which SDK events are sent to Superwall. :::flutter Flutter currently uses `isExternalDataCollectionEnabled`. Setting it to `false` suppresses user-initiated tracking, trigger-fire events, and user-attribute updates while keeping internal Superwall event collection enabled. ```dart SuperwallOptions options = SuperwallOptions(); options.isExternalDataCollectionEnabled = false; Superwall.configure( "MY_API_KEY", options: options ); ``` ::: ### Automatically Dismissing the Paywall By default, Superwall automatically dismisses the paywall when a product is purchased or restored. You can disable this by setting `automaticallyDismiss` to `false`: :::flutter ```dart SuperwallOptions options = SuperwallOptions(); options.paywalls.automaticallyDismiss = false; Superwall.configure( "MY_API_KEY", options: options ); ``` ::: To manually dismiss the paywall , call `Superwall.shared.dismiss()`. ### Custom Restore Failure Message You can set the title, message and close button title for the alert that appears after a restoration failure: :::flutter ```dart SuperwallOptions options = SuperwallOptions(); options.paywalls.restoreFailed.title = "My Title"; options.paywalls.restoreFailed.message = "My message"; options.paywalls.restoreFailed.closeButtonTitle = "Close"; Superwall.configure( "MY_API_KEY", options: options ); ``` ::: ### Haptic Feedback On iOS, the paywall uses haptic feedback by default after a user purchases or restores a product, opens a URL from the paywall, or closes the paywall. To disable this, set the `isHapticFeedbackEnabled` `PaywallOption` to false: :::flutter ```dart SuperwallOptions options = SuperwallOptions(); options.paywalls.isHapticFeedbackEnabled = false; Superwall.configure( "MY_API_KEY", options: options ); ``` ::: Note: Android does not use haptic feedback. ### Transaction Background View During a transaction, we add a `UIActivityIndicator` behind the view to indicate a loading status. However, you can remove this by setting the `transactionBackgroundView` to `nil`: :::flutter ```dart SuperwallOptions options = SuperwallOptions(); options.paywalls.transactionBackgroundView = TransactionBackgroundView.none; Superwall.configure( "MY_API_KEY", options: options ); ``` ::: ### Purchase Failure Alert When a purchase fails, we automatically present an alert with the error message. If you'd like to show your own alert after failure, set the `shouldShowPurchaseFailureAlert` `PaywallOption` to `false`: :::flutter ```dart SuperwallOptions options = SuperwallOptions(); options.paywalls.shouldShowPurchaseFailureAlert = false; Superwall.configure( "MY_API_KEY", options: options ); ``` ::: ### Web Purchase Confirmation Alert When a user completes a purchase via web checkout (app2web flow), you can control whether to show a confirmation alert. By default, this is set to `false` to prevent duplicate alerts. Set `shouldShowWebPurchaseConfirmationAlert` to `true` if you want to show the native confirmation alert: :::flutter ```dart SuperwallOptions options = SuperwallOptions(); options.paywalls.shouldShowWebPurchaseConfirmationAlert = true; Superwall.configure( "MY_API_KEY", options: options ); ``` ::: ### Locale Identifier When evaluating rules, the device locale identifier is set to `autoupdatingCurrent`. However, you can override this if you want to test a specific locale: :::flutter ```dart SuperwallOptions options = SuperwallOptions(); options.localeIdentifier = "en_GB"; Superwall.configure( "MY_API_KEY", options: options ); // Or you can set: Superwall.shared.setLocaleIdentifier("en_GB"); // To revert to default: Superwall.shared.setLocaleIdentifier(null); ``` ::: For a list of locales that are available on iOS, take a look at [this list](https://gist.github.com/jacobbubu/1836273). You can also preview your paywall in different locales using [In-App Previews](/docs/sdk/quickstart/in-app-paywall-previews). ### Game Controller If you're using a game controller, you can enable this in `SuperwallOptions` too. Check out our [Game Controller Support](/docs/sdk/guides/advanced/game-controller-support) article. Take a look at [SuperwallOptions](https://sdk.superwall.me/documentation/superwallkit/superwalloptions) in our SDK reference for more info. # 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 # Consumable Products Set up consumable products for Superwall paywalls in Flutter apps. Use consumable products when a purchase should grant a quantity that can be used up, such as credits, coins, boosts, or tokens. This guide covers the Superwall-only flow where purchases are started from paywalls and you are not using a `PurchaseController`. The platform sections below explain the iOS and Android store requirements that still apply to Flutter apps. ## iOS > **Note:** This guide assumes purchases are made from Superwall paywalls and that you are not using a `PurchaseController`. Consumable products are one-time purchases that users can buy repeatedly, such as credits, tokens, boosts, or packs. Non-consumable products are also one-time purchases, but they grant permanent access, such as a lifetime unlock. Superwall uses entitlements to decide whether a user has ongoing access. Because consumables are meant to be used up, they should usually not grant entitlements. Your app should listen for the purchase, grant the consumable benefit in your own system, and treat Superwall's purchase history as a record of what happened. ## Dashboard Setup 1. Create the consumable in App Store Connect. 2. Add the product in Superwall from **Products**. 3. Use the App Store product identifier. 4. Set **Period** to **None (Lifetime / Consumable)**. 5. Leave **Entitlements** empty. 6. Add the product to any paywall that should sell it. > **Warning:** Do not attach an entitlement to a consumable unless the purchase should also unlock ongoing access. If a consumable has no entitlement, buying it does not make the user's subscription status active. ## Include Consumables In Purchase History Apple excludes consumable purchases from App Store purchase history unless you opt in. Add `SKIncludeConsumableInAppPurchaseHistory` to your app's `Info.plist` as a Boolean set to `YES`. ```xml Info.plist SKIncludeConsumableInAppPurchaseHistory ``` > **Note:** When this key is present and set to `YES`, Superwall uses StoreKit 2 on iOS 18 and later. On earlier iOS versions, the SDK falls back to StoreKit 1 for purchase history support. ## Grant The Consumable Benefit Superwall does not maintain balances for consumables. Grant credits, tokens, or other benefits from your app or backend after the `transactionComplete` event. Make this operation idempotent so retries do not double-credit the user. ## Tab ```swift Swift import SuperwallKit final class SWDelegate: SuperwallDelegate { func handleSuperwallEvent(withInfo eventInfo: SuperwallEventInfo) { guard case let .transactionComplete(transaction, product, _, _) = eventInfo.event else { return } guard product.productIdentifier == "com.example.credits_100" else { return } Task { await ConsumablesService.shared.grantCredits( count: 100, productId: product.productIdentifier, transactionId: transaction?.storeTransactionId ) } } } Superwall.shared.delegate = SWDelegate() ``` ## Tab ```swift Objective-C #import @interface SWDelegate : NSObject @end @implementation SWDelegate - (void)handleSuperwallEventWithInfo:(SWKSuperwallEventInfo *)eventInfo { if (eventInfo.event != SWKSuperwallEventTransactionComplete) { return; } NSString *productId = eventInfo.params[@"primary_product_id"]; if (![productId isEqualToString:@"com.example.credits_100"]) { return; } NSString *transactionId = eventInfo.params[@"store_transaction_id"]; [[ConsumablesService shared] grantCredits:100 productId:productId transactionId:transactionId]; } @end [Superwall sharedInstance].delegate = [SWDelegate new]; ``` ## Read Purchase History Consumable and non-consumable purchases appear in `customerInfo.nonSubscriptions`. Use `isConsumable` to distinguish consumables from lifetime purchases. ## Tab ```swift Swift let customerInfo = Superwall.shared.customerInfo let consumables = customerInfo.nonSubscriptions.filter { $0.isConsumable } for purchase in consumables { print("Consumable purchased: \(purchase.productId)") } ``` ## Tab ```swift Objective-C SWKCustomerInfo *customerInfo = [Superwall sharedInstance].customerInfo; for (SWKNonSubscriptionTransaction *purchase in customerInfo.nonSubscriptions) { if (purchase.isConsumable) { NSLog(@"Consumable purchased: %@", purchase.productId); } } ``` ## Android > **Note:** This guide assumes purchases are made from Superwall paywalls and that you are not using a `PurchaseController`. Consumable products are one-time purchases that users can buy repeatedly, such as credits, tokens, boosts, or packs. Non-consumable products are also one-time purchases, but they grant permanent access, such as a lifetime unlock. Superwall uses entitlements to decide whether a user has ongoing access. Because consumables are meant to be used up, they should usually not grant entitlements. Your app should listen for the purchase, grant the consumable benefit in your own system, and then consume the Google Play purchase token so the item can be purchased again. ## Dashboard Setup 1. Create the product as an in-app product in Google Play Console. 2. Add the product in Superwall from **Products**. 3. Use the Google Play product ID. 4. Set **Period** to **None (Lifetime / Consumable)**. 5. Leave **Entitlements** empty. 6. Add the product to any paywall that should sell it. > **Warning:** Do not attach an entitlement to a consumable unless the purchase should also unlock ongoing access. If a consumable has no entitlement, buying it does not make the user's subscription status active. ## Consume The Purchase Token Google Play in-app products must be consumed after you grant the benefit. If you do not consume the purchase token, the user may not be able to buy that same consumable again. Use the `purchaseToken` from the `TransactionComplete` event, grant the benefit, then call `Superwall.instance.consume(purchaseToken)`. > **Note:** `Superwall.instance.consume(purchaseToken)` is available in Android SDK 2.6.2 and later. ## Tab ```kotlin Kotlin import androidx.lifecycle.lifecycleScope import com.superwall.sdk.Superwall import com.superwall.sdk.analytics.superwall.SuperwallEvent import com.superwall.sdk.analytics.superwall.SuperwallEventInfo import com.superwall.sdk.delegate.SuperwallDelegate import kotlinx.coroutines.launch class MainActivity : AppCompatActivity(), SuperwallDelegate { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Superwall.instance.delegate = this } override fun handleSuperwallEvent(eventInfo: SuperwallEventInfo) { val event = eventInfo.event if (event !is SuperwallEvent.TransactionComplete) { return } if (event.product.productIdentifier != "coins_100") { return } val purchaseToken = event.transaction?.purchaseToken ?: return lifecycleScope.launch { ConsumablesService.grantCoins( count = 100, productId = event.product.productIdentifier, purchaseToken = purchaseToken, ) Superwall.instance.consume(purchaseToken) .onFailure { error -> // Retry consumption after confirming the benefit was granted. println("Failed to consume purchase: ${error.message}") } } } } ``` ## Tab ```java Java import com.superwall.sdk.Superwall; import com.superwall.sdk.analytics.superwall.SuperwallEvent; import com.superwall.sdk.analytics.superwall.SuperwallEventInfo; import com.superwall.sdk.delegate.SuperwallDelegateJava; import com.superwall.sdk.store.abstractions.transactions.StoreTransactionType; import kotlin.Unit; public class MainActivity extends AppCompatActivity implements SuperwallDelegateJava { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Superwall.getInstance().setJavaDelegate(this); } @Override public void handleSuperwallEvent(SuperwallEventInfo eventInfo) { if (!(eventInfo.getEvent() instanceof SuperwallEvent.TransactionComplete)) { return; } SuperwallEvent.TransactionComplete event = (SuperwallEvent.TransactionComplete) eventInfo.getEvent(); if (!event.getProduct().getProductIdentifier().equals("coins_100")) { return; } StoreTransactionType transaction = event.getTransaction(); if (transaction == null) { return; } String purchaseToken = transaction.getPurchaseToken(); ConsumablesService.grantCoins(100, event.getProduct().getProductIdentifier(), purchaseToken); Superwall.getInstance().consume(purchaseToken, result -> { // Check the Result in your app and retry if consumption fails. return Unit.INSTANCE; }); } } ``` > **Warning:** Grant the benefit before consuming the token. If your grant call fails, leave the purchase unconsumed and retry after your app confirms the user received the benefit. ## Read Purchase History Consumable and non-consumable purchases appear in `customerInfo.nonSubscriptions`. Use `isConsumable` to distinguish consumables from lifetime purchases. ## Tab ```kotlin Kotlin val customerInfo = Superwall.instance.getCustomerInfo() val consumables = customerInfo.nonSubscriptions.filter { it.isConsumable } for (purchase in consumables) { println("Consumable purchased: ${purchase.productId}") } ``` ## Tab ```java Java CustomerInfo customerInfo = Superwall.getInstance().getCustomerInfo(); for (NonSubscriptionTransaction purchase : customerInfo.getNonSubscriptions()) { if (purchase.isConsumable()) { System.out.println("Consumable purchased: " + purchase.getProductId()); } } ``` ## Flutter Event Handling In Flutter, listen for purchase events with [`SuperwallDelegate`](/docs/flutter/sdk-reference/SuperwallDelegate) or a [`PaywallPresentationHandler`](/docs/flutter/sdk-reference/PaywallPresentationHandler). Android consumables still need the Google Play purchase token consumed after you grant the benefit. Use [`Superwall.shared.consume(purchaseToken)`](/docs/flutter/sdk-reference/consume) for the Android consumption call. The current Flutter SDK exposes `consume()` directly, but its public `StoreTransaction` wrapper does not expose the Android `purchaseToken` from Superwall's `transactionComplete` event. If your Flutter app sells Android consumables, expose that token from your Android purchase handling path before calling `consume()`. # 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 # Experimental Flags Experimental flags in Superwall's SDK allow you to opt into features that are safe for production but are still being refined. These features may undergo naming changes or internal restructuring in future SDK versions. We expose them behind flags to give you early access while preserving flexibility for ongoing development. These flags are configured via the `SuperwallOptions` struct: ```swift let options = SuperwallOptions() options.enableExperimentalDeviceVariables = true Superwall.configure(apiKey: "my_api_key", options: options) ``` ## Available experimental flags When these flags are enabled and the user runs your app, these values become available in campaign filters. Currently, these include: **Latest Subscription Period Type (String)**: Represents whether the user is in a trial, promotional, or a similar phase. Possible values include: * `trial` * `code` * `subscription` * `promotional` * `winback` * `revoked` Represented as `latestSubscriptionPeriodType` in campaign filters. **Latest Subscription State (String)**: Represents what *state* the actual subscription is in. Possible values include: * `inGracePeriod` * `subscribed` * `expired` * `inBillingRetryPeriod` * `revoked` Represented as `latestSubscriptionState` in campaign filters. **Latest Subscription Will Auto Renew (Bool)**: If the user is set to renew or not. Either `true` or `false` Represented as `latestSubscriptionWillAutoRenew` in campaign filters. ### Detecting users who've cancelled an active trial One common use case for these flags is detecting users who've cancelled an active trial. In that case, the filter in the campaign would check for `latestSubscriptionWillAutoRenew` to be `false` and `latestSubscriptionPeriodType` to be `trial`. :::flutter ### Platform Availability These variables are currently only available on **iOS**, support for Android is not yet available. ::: # 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 # Handling Deep Links Use handleDeepLink and campaign rules to present paywalls from deep links without hardcoding logic in your app. When your app receives a deep link, you might be tempted to write a switch statement that maps each URL to a specific placement and calls `register`. This works, but it means every time you add a new link or change which paywall shows, you have to ship an app update. A better approach is to pass the URL to `handleDeepLink` and let Superwall's [`deepLink_open`](/docs/dashboard/dashboard-campaigns/campaigns-standard-placements#deeplink_open) standard placement handle the rest. The SDK extracts the URL's path, query parameters, and other components, then fires `deepLink_open` as a placement. You write campaign rules on the dashboard to decide which paywall to show, which means there is no app update required. ## The problem Here's a common pattern where deep link routing is hardcoded in the app: :::flutter ```dart void handleUrl(Uri url) { final placement = switch (url.path) { '/promo' => 'promoPlacement', '/onboarding' => 'onboardingPlacement', '/upgrade' => 'upgradePlacement', '/special-offer' => 'specialOfferPlacement', _ => null, }; if (placement != null) { Superwall.shared.register(placement: placement); } } ``` ::: Every new URL path means a code change, a build, and an app store review. If you want to change which paywall shows for `/promo`, that's another update too. ## The solution: `handleDeepLink` + campaign rules Instead, pass the URL to `handleDeepLink`. The SDK fires the `deepLink_open` standard placement with all of the URL's components as parameters. Then, on the Superwall dashboard, you create campaign rules that match on those parameters to decide what to show. :::flutter ```dart Future handleUrl(Uri url) async { await Superwall.shared.handleDeepLink(url); } ``` ::: That's it on the app side. The routing logic lives on the dashboard. ## Setting up campaign rules Once `handleDeepLink` is wired up, the `deepLink_open` placement fires every time a deep link arrives. The URL's path, host, query parameters, and other components are available as parameters you can match against in your campaign's audience filters. ## Create a campaign On the Superwall dashboard, create a new [campaign](/docs/dashboard/dashboard-campaigns/campaigns) — for example, "Deep Link Paywalls". ## Add the deepLink_open placement In your campaign, [add a placement](/docs/dashboard/dashboard-campaigns/campaigns-placements#adding-a-placement) and select `deepLink_open` from the standard placements list. ## Add audience filters Edit the default audience and add filters that match the URL components you care about. For example, if your deep link is `myapp://promo?offer=summer`:- Set `params.path` **is** `promo` to match the path. - Set `params.offer` **is** `summer` to match the query parameter.See [`deepLink_open` parameters](/docs/dashboard/dashboard-campaigns/campaigns-standard-placements#deeplink_open) for the full list of available fields. ## Attach a paywall Click **Paywalls** at the top of the campaign and choose which paywall to present when the filters match. Now when a user opens `myapp://promo?offer=summer`, the SDK fires `deepLink_open`, the campaign rule matches, and the paywall shows. That's all without touching your app code. To add a new deep link path or change which paywall it shows, just update the campaign on the dashboard. ## Multiple deep link routes You can handle several deep link patterns from a single campaign by adding multiple audiences, each with its own filters and paywalls. For example: | Deep link | Filter | Paywall | | ----------------------------- | -------------------------------------------------------- | --------------- | | `myapp://promo?offer=summer` | `params.path` is `promo` AND `params.offer` is `summer` | Summer Sale | | `myapp://promo?offer=newyear` | `params.path` is `promo` AND `params.offer` is `newyear` | New Year Offer | | `myapp://upgrade` | `params.path` is `upgrade` | Upgrade Paywall | Each audience evaluates independently. When you need to add a new route, create a new audience on the dashboard — no app update needed. ## Prerequisites To use `handleDeepLink`, your app needs deep link handling set up first. If you haven't done that yet, follow the setup guide: :::flutter * [Deep link setup](/docs/sdk/quickstart/in-app-paywall-previews) ::: ## Related deep link guides :::flutter * [Deep Link Setup](/docs/sdk/quickstart/in-app-paywall-previews) — Configure URL schemes, universal links, and wire `handleDeepLink` into your app so Superwall can respond to incoming links. * [Using Superwall Deep Links](/docs/sdk/guides/superwall-deep-links) — Trigger paywalls or custom in-app behavior using Superwall-hosted URLs at `*.superwall.app/app-link/...`. ::: # 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 # Migrating from v1 to v2 - Flutter SuperwallKit 2.0 is a major release of Superwall's Flutter SDK. This introduces breaking changes. ## Migration steps ## 1\. Update code references ### 1.1 Rename references from `event` to `placement` In some cases, you should be able to update references using the automatic renaming suggestions that Xcode provides. For other cases where this hasn't been possible, you'll need to run through this list to manually update your code. | Before | After | | ------------------------------------ | ---------------------------------------- | | fun registerEvent(event:) | fun registerPlacement(placement:) | | fun preloadPaywalls(forEvents:) | fun preloadPaywalls(forPlacements:) | | fun getPaywall(forEvent:) | fun getPaywall(forPlacement:) | | fun getPresentationResult(forEvent:) | fun getPresentationResult(forPlacement:) | | TriggerResult.eventNotFound | TriggerResult.placementNotFound | ## 2\. SuperwallBuilder To make tracking and reacting to subscription status changes easier, we've introduced a new `SuperwallBuilder` widget. Using it is quite simple - just add it to your widget tree and every time the subscription status is changed, the builder function will be invoked, triggering a re-render of it's child widgets. For example, here's a simple implementation that will change the text based on the subscription status: ```dart Flutter SuperwallBuilder( builder: (context, status) => Center( child: Text('Subscription Status: ${status}'), ) ) ``` ### 3\. Getting the purchased product The `onDismiss` block of the `PaywallPresentationHandler` now accepts both a `PaywallInfo` object and a `PaywallResult` object. This allows you to easily access the purchased product from the result when the paywall dismisses. ### 4\. Entitlements The `subscriptionStatus` has been changed to accept a set of `Entitlement` objects. This allows you to give access to entitlements based on products purchased. For example, in your app you might have Bronze, Silver, and Gold subscription tiers, i.e. entitlements, which entitle a user to access a certain set of features within your app. Every subscription product must be associated with one or more entitlements, which is controlled via the dashboard. Superwall will already have associated all your products with a default entitlement. If you don't use more than one entitlement tier within your app and you only use subscription products, you don't need to do anything extra. However, if you use one-time purchases or multiple entitlements, you should review your products and their entitlements. In general, consumables should not be associated with an entitlement, whereas non-consumables should be. Check your products [here](https://superwall.com/applications/\:app/products/v2). If you're using a `PurchaseController`, you'll need to set the `entitlements` with the `subscriptionStatus`: | Before | After | | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | | Superwall.shared.setSubscriptionStatus(SubscriptionStatus.active) | Superwall.shared.setSubscriptionStatus(SubscriptionStatusActive(entitlements: entitlements)) | Here is an example of how you'd sync your subscription status with Superwall using these methods: ## Tab ```dart RevenueCat // Only necessary if you're using a PurchaseController. // Otherwise, Superwall does this automatically. fun syncSubscriptionStatus() { Purchases.addCustomerInfoUpdateListener((customerInfo) async { // Gets called whenever new CustomerInfo is available final entitlements = customerInfo.entitlements.active.keys .map((id) => Entitlement(id: id)) .toSet(); final hasActiveEntitlementOrSubscription = customerInfo .hasActiveEntitlementOrSubscription(); // Why? -> https://www.revenuecat.com/docs/entitlements#entitlements if (hasActiveEntitlementOrSubscription) { await Superwall.shared.setSubscriptionStatus( SubscriptionStatusActive(entitlements: entitlements)); } else { await Superwall.shared .setSubscriptionStatus(SubscriptionStatusInactive()); } }); } ``` You can listen to the published property `Superwall.shared.subscriptionStatus` to be notified when the subscriptionStatus changes. Or you can use the `SuperwallDelegate` method `subscriptionStatusDidChange(from:to:)`, which replaces `subscriptionStatusDidChange(to:)`. ### 5\. Paywall Presentation Condition In the Paywall Editor you can choose whether to always present a paywall or ask the SDK to check the user subscription before presenting a paywall. For users on v1 of the SDK, this is replaced with a check on the entitlements within the audience filter. As you migrate your users from v1 to v2 of the SDK, you'll need to make sure you set both the entitlements check and the paywall presentation condition in the paywall editor. ![](/docs/images/camp-presentation-conditions.png) ## 6\. Check out the full change log You can view this on [our GitHub page](https://github.com/superwall/Superwall-Flutter/blob/main/CHANGELOG.md). ## 7\. Check out our updated example apps All of our [example apps](https://github.com/superwall/Superwall-Flutter/tree/main/example) have been updated to use the latest SDK. We now only have two apps: Basic and Advanced. Basic shows you the basic integration of Superwall without needing a purchase controller or multiple entitlements. Advanced shows you how to use entitlements within your app as well as optionally using a purchase controller with StoreKit or RevenueCat. # 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 Superwall Deep Links (iOS only) How to use Superwall Deep Links to trigger paywalls or custom in-app behavior. A Superwall Deep Link is a URL hosted at `https://.superwall.app/app-link/...` that opens your app to trigger a paywall as configured on the Superwall dashboard, or custom in-app behavior via the Superwall delegate. ## Prerequisites :::flutter 1. Set up [deep link handling](/docs/sdk/quickstart/in-app-paywall-previews) ::: 2. Create a [Web Checkout app](/docs/web-checkout/web-checkout-creating-an-app), even if you do not plan to charge through Web Checkout, this provisions the `*.superwall.app` domain that powers Superwall Deep Links. ## Handling incoming links * Always call `handleDeepLink` first. It returns `true` when the SDK recognizes the URL and plans to take over presentation, or `false` when you should continue routing inside your own app. * When a recognized link arrives before `Superwall.configure(...)` finishes, the SDK caches it and replays it immediately after configuration completes, so it is safe to forward links during cold launch. * If the return value is `false`, continue with your normal router—those links are not associated with any Superwall experience. :::flutter ```dart Future _handleIncomingLink(Uri uri) async { final handled = await Superwall.shared.handleDeepLink(uri); if (!handled) { _routeInternally(uri); } } void _listenForLinks() { uriLinkStream.listen((uri) { if (uri != null) { _handleIncomingLink(uri); } }); } ``` ::: ## Link formats and campaigns Deep link URLs are hosted at `https://.superwall.app/app-link/...`, you can have anything after the `/app-link/` path, including query parameters. These values will be availble to you in audience filters on the Superwall dashboard, or in the `handleSuperwallDeepLink` delegate method. :::flutter ```dart class PaywallDelegate extends SuperwallDelegate { @override void handleSuperwallDeepLink( Uri fullURL, List pathComponents, Map queryParameters, ) { if (pathComponents.isEmpty) { return; } switch (pathComponents.first) { case 'campaign': final placementId = pathComponents.length > 1 ? pathComponents[1] : null; if (placementId != null) { _routeToPlacement(placementId, queryParameters); } break; default: break; } } } ``` ::: Keep your own routing logic in place for non-Superwall URLs and for any additional behaviors you want to stack on top of Superwall's default presentation flow. ## Related deep link guides :::flutter * [Deep Link Setup](/docs/sdk/quickstart/in-app-paywall-previews) — Configure URL schemes, universal links, and wire `handleDeepLink` into your app so Superwall can respond to incoming links. * [Handling Deep Links](/docs/sdk/guides/handling-deep-links) — Use `handleDeepLink` with the `deepLink_open` standard placement and dashboard campaign rules to present paywalls from your own deep links, without hardcoding routing logic. ::: # 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 # StoreKit testing (iOS only) How to set up StoreKit testing for iOS when using the Flutter SDK. StoreKit testing in Xcode is a local test environment for testing in-app purchases without requiring a connection to App Store servers. Set up in-app purchases in a local StoreKit configuration file in your Xcode project, or create a synced StoreKit configuration file in Xcode from your in-app purchase settings in App Store Connect. After you enable the configuration file, the test environment uses this local data on your paywalls when your app calls StoreKit APIs. ### Add a StoreKit Configuration File Go to **File ▸ New ▸ File...** in the menu bar , select **StoreKit Configuration File** and hit **Next**: ![](/docs/images/3dbedb3-Screenshot_2023-03-02_at_11.35.16.png) Give it the name **Products**. For a configuration file synced with an app on App Store Connect, select the checkbox, specify your team and app in the drop-down menus that appear, then click **Next**. For a local configuration, leave the checkbox unselected, then click **Next**. Save the file in the top-level folder of your project. You don't need to add it to your target. ### Create a New Scheme for StoreKit Testing It's best practice to create a new scheme in Xcode to be used for StoreKit testing. This allows you to separate out staging and production environments. Click the scheme in the scheme menu and click **Manage Schemes...**: ![](/docs/images/e0e2c89-Screenshot_2023-03-02_at_11.44.02.png) If you haven't already got a Staging scheme, select your current scheme and click **Duplicate**: ![](/docs/images/d3c7e96-Screenshot_2023-03-02_at_11.44.47.png) In the scheme editor, add the StoreKit Configuration file to your scheme by clicking on **Run** in the side bar, selecting the **Options** tab and choosing your configuration file in **StoreKit Configuration**. Then, click **Close**: ![](/docs/images/444719d-Screenshot_2023-03-02_at_11.46.34.png) You can rename your scheme to **MyAppName (Staging)**. ### Setting up the StoreKit Configuration File If you've chosen to sync your configuration file with the App Store, your apps will automatically be loaded into your StoreKit Configuration file. When you add new products, just sync again. If you're using a local configuration, open **Products.storekit**, click the **+** button at the bottom and create a new product. In this tutorial, we'll create an auto-renewable subscription: ![](/docs/images/6d89a21-Screenshot_2023-03-02_at_12.07.50.png) Enter a name for a new subscription group and click **Done**. The subscription group name should match one that is set up for your app in App Store Connect, but it's not a requirement. That means you can test your subscription groups and products in the simulator and then create the products in App Store Connect later: ![](/docs/images/717d912-Screenshot_2023-03-02_at_12.09.07.png) Configure the subscription as needed by filling in the **Reference Name**, **Product ID**, **Price**, **Subscription Duration**, and optionally an **Introductory Offer**. Again, this product doesn't have to exist in App Store Connect for you to test purchasing in the simulator. Here is a sample configuration: ![](/docs/images/d6a9b7f-Screenshot_2023-03-02_at_12.10.27.png) Repeat this for all of your products. When configuring a paywall, the product ID you enter here must match the product ID on the paywall. You're now all set! ## Testing purchases with Transaction Manager Once you've set up your StoreKit configuration file, you can leverage Xcode's Transaction Manager. Find it under **Debug -> StoreKit -> Manage Transactions...**: ![](/docs/images/transactionManager.png) Use this to quickly test purchasing your products. Once you make a purchase, you can open Transaction Manager to delete it, refund it, request parental approval and much more. Most commonly, you'll probably delete the transaction to reset your subscription state: ![](/docs/images/transactionRemove.png) This makes everything a little faster, saving you the trouble of having to delete and reinstall your app to test these states. If you'd like to see a video over how to use it, check this one out: