# KMP 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 KMP SDK # CHANGELOG The changelog for `Superwall-KMP`. Also see the [releases](https://github.com/superwall/Superwall-KMP/releases) on GitHub. ## 0.1.1 ## Enhancements * Adds threading improvements to reduce main thread load ## 0.1.0 Initial release of the Kotlin Multiplatform SDK for Superwall. ## Enhancements * Adds `com.superwall.sdk:superwall-kmp`, a Kotlin Multiplatform wrapper over the native Superwall SDKs. The entire public API lives in `commonMain` — no platform types leak into it, and `Superwall.configure` has an identical signature on both platforms (no `Context` parameter on Android). * Android support (`minSdk 26`) wrapping `com.superwall.sdk:superwall-android` 2.8.0. The dependency is bundled transitively and an `androidx.startup` initializer captures the `Application`, so integration is a single Gradle dependency. * iOS support (iOS 14+, `iosArm64`/`iosSimulatorArm64`/`iosX64`) forwarding through **SuperwallKMPBridge**, a self-authored `@objc` Swift facade over SuperwallKit iOS 4.16.1 (pinned exactly). The bridge destructures Swift-only constructs — enum associated values, structs, `async` — into ObjC-visible envelopes consumed via cinterop. * Adds `Superwall.register(placement:params:handler:feature:)` for gating features behind paywalls, with a `PaywallPresentationHandler` exposing `onPresent`, `onDismiss`, `onError` and `onSkip`. * Adds `Superwall.subscriptionStatusFlow`, a `StateFlow` that is collectable before `configure` (seeded with `SubscriptionStatus.Unknown`) and emits on the main thread. * Adds `PurchaseController` for apps that own their purchase logic, with separate `purchaseFromAppStore` and `purchaseFromGooglePlay` entry points. * Adds `SuperwallDelegate` covering the paywall presentation lifecycle, subscription-status changes, deep links, URLs, custom paywall actions, logging, and link redemption. * Adds `SuperwallOptions` (including `PaywallOptions` and `TestModeBehavior`), user identity (`identify`, `reset`, user attributes), and deep-link handling via `handleDeepLink`. * Adds `configureAndAwait`, a suspending twin of `configure`, plus the `Superwall.isConfigured` flag for ordering calls. ## Notes * There is no pre-configure call queue: most members throw `SuperwallError.NotConfigured` before `configure`. Guard-exempt members are `handleDeepLink`, the flows, `delegate`, and the introspection properties. * iOS integration is two steps — the Gradle dependency plus the `SuperwallKMPBridge` Swift package — and the Kotlin framework must be exported as `isStatic = true`, because it does not embed the bridge binary. See the [README](README.md#ios) for details. # 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 Forward Superwall events to your own analytics stack. > **Warning:** **Beta**The KMP SDK is in beta and its API may change between releases. Superwall tracks events internally: paywalls opening, transactions completing, placements firing. You can forward all of them to your own analytics provider through `SuperwallDelegate`. ## Forwarding events Implement `handleSuperwallEvent`: ```kotlin import com.superwall.sdk.kmp.SuperwallDelegate import com.superwall.sdk.kmp.models.events.SuperwallEventInfo class AnalyticsDelegate : SuperwallDelegate { override fun handleSuperwallEvent(eventInfo: SuperwallEventInfo) { analytics.track( name = eventInfo.eventType.name, properties = eventInfo.params.orEmpty(), ) } } Superwall.delegate = AnalyticsDelegate() ``` > **Warning:** `handleSuperwallEvent` is **not guaranteed** to arrive on any particular thread, and that may change between releases. Do not touch UI from it without hopping to main yourself, and do not assume it is off the main thread either. Keep the body cheap and non-blocking. See [Platform differences](/docs/kmp/guides/platform-differences#delegate-threading). ## The event envelope `SuperwallEventInfo` is a flat envelope. `eventType` identifies the event, and only the fields relevant to that event are non-null: ```kotlin import com.superwall.sdk.kmp.models.events.EventType override fun handleSuperwallEvent(eventInfo: SuperwallEventInfo) { when (eventInfo.eventType) { EventType.PAYWALL_OPEN -> { analytics.track("paywall_open", mapOf( "paywall_id" to eventInfo.paywallInfo?.identifier, "paywall_name" to eventInfo.paywallInfo?.name, )) } EventType.TRANSACTION_COMPLETE -> { analytics.track("purchase", mapOf( "product_id" to eventInfo.product?.productIdentifier, )) } else -> analytics.track(eventInfo.eventType.name, eventInfo.params.orEmpty()) } } ``` Commonly useful fields on the envelope: ## Sending your identifiers to Superwall The reverse direction matters too, since Superwall can attribute better if it knows your analytics identifiers: ```kotlin import com.superwall.sdk.kmp.models.events.IntegrationAttribute Superwall.setIntegrationAttributes( mapOf( IntegrationAttribute.AMPLITUDE_USER_ID to amplitude.userId, IntegrationAttribute.MIXPANEL_DISTINCT_ID to mixpanel.distinctId, IntegrationAttribute.APPSFLYER_ID to appsFlyer.uid, ), ) ``` Supported providers include Adjust, Amplitude, AppsFlyer, Braze, OneSignal, Meta, Firebase, Singular, Iterable, Mixpanel, mParticle, CleverTap, Airship, Kochava, Tenjin, PostHog, Customer.io, and Appstack. Passing `null` for a value removes it. > **Note:** `IntegrationAttribute.FIREBASE_INSTALLATION_ID` is **iOS only**. Setting it on Android is skipped > and logs a warning. Every other attribute works on both platforms. ## Capturing SDK logs `handleLog` gives you the SDK's own log stream: ```kotlin override fun handleLog( level: LogLevel, scope: LogScope, message: String?, info: Map?, error: String?, ) { if (level == LogLevel.ERROR) { crashReporter.log("Superwall/${scope.name}: $message") } } ``` > **Warning:** `handleLog` fires for **every** internal log line, regardless of the configured log level, which is > hundreds of calls for a single `register`. Filter early, keep the body cheap, and never block in > it. ## Controlling what Superwall collects To limit what leaves the device, set `eventTrackingBehavior`: ```kotlin Superwall.configure( apiKey = "pk_your_api_key", options = SuperwallOptions( eventTrackingBehavior = EventTrackingBehavior.SUPERWALL_ONLY, ), ) ``` | Value | Effect | | ---------------- | -------------------------------------------------------------------------------------------------------------------- | | `ALL` | Everything is tracked. The default. | | `SUPERWALL_ONLY` | Only internal Superwall events; your tracking calls, trigger-fire events, and user-attribute updates are suppressed. | | `NONE` | Nothing is sent to Superwall's servers. | # 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 # Purchases and Subscription Status Own your purchase logic with a PurchaseController. > **Warning:** **Beta**The KMP SDK is in beta and its API may change between releases. By default Superwall handles purchases and subscription status for you, and most apps should leave it that way. If you already have purchase logic (your own billing stack, or a provider like RevenueCat), you can take it over with a `PurchaseController`. > **Note:** Passing a `PurchaseController` means **you** own subscription status. Superwall will not set it for > you, so you must set `Superwall.subscriptionStatus` yourself after every purchase, restore, and app > launch. ## The interface One interface covers both stores. Each platform invokes only its own store's method, so you implement all three and only two ever run on a given device. ```kotlin import com.superwall.sdk.kmp.PurchaseController import com.superwall.sdk.kmp.models.results.PurchaseResult import com.superwall.sdk.kmp.models.results.RestorationResult class MyPurchaseController : PurchaseController { override suspend fun purchaseFromAppStore(productId: String): PurchaseResult { // Your StoreKit logic return PurchaseResult.Purchased } override suspend fun purchaseFromGooglePlay( productId: String, basePlanId: String?, offerId: String?, ): PurchaseResult { // Your Play Billing logic return PurchaseResult.Purchased } override suspend fun restorePurchases(): RestorationResult { // Your restore logic return RestorationResult.Restored } } ``` All three are `suspend` functions, so you can do the real asynchronous work inline without callbacks. > **Note:** `purchaseFromGooglePlay` takes `basePlanId` and `offerId` alongside the product id, because Play > models subscriptions with base plans and offers. iOS has no equivalent, which is why the two entry > points are separate rather than one method with platform-shaped arguments. ## Handling every case `PurchaseResult` is a sealed interface, so handle all four: | Result | When | | --------------- | ------------------------------------------------------------------------------------------ | | `Purchased` | The product was purchased | | `Cancelled` | The user cancelled. StoreKit 2's `.userCancelled`, or RevenueCat's `userCancelled == true` | | `Pending` | Awaiting action. StoreKit 1's `.deferred`, or RevenueCat's `paymentPendingError` | | `Failed(error)` | Anything else | ```kotlin override suspend fun purchaseFromAppStore(productId: String): PurchaseResult { return try { when (val outcome = myStore.purchase(productId)) { is Success -> PurchaseResult.Purchased is UserCancelled -> PurchaseResult.Cancelled is Deferred -> PurchaseResult.Pending is Error -> PurchaseResult.Failed(outcome.message) } } catch (e: Exception) { PurchaseResult.Failed(e.message ?: "Unknown error") } } ``` There are convenience factories if you prefer them: `PurchaseResult.purchased()`, `.cancelled()`, `.pending()`, `.failed(error)`. `RestorationResult` has two cases, `Restored` and `Failed(error)`. > **Warning:** `RestorationResult.Restored` means the restore completed **without errors**, not that the user has > an active subscription. Set subscription status from the entitlements you actually resolved, not > from the fact that restore succeeded. ## Wire it up Pass the controller at configure time: ```kotlin Superwall.configure( apiKey = "pk_your_api_key", purchaseController = MyPurchaseController(), ) ``` > **Note:** A second `configure` call will **not** install a different purchase controller, because repeat calls are > a no-op. Set it on the first call. ## Keep subscription status current This is the part that is easy to forget. After any purchase, restore, or launch-time entitlement check, tell Superwall what you found: ```kotlin import com.superwall.sdk.kmp.models.entitlements.Entitlement import com.superwall.sdk.kmp.models.entitlements.SubscriptionStatus fun syncSubscriptionStatus(activeEntitlementIds: Set) { Superwall.subscriptionStatus = if (activeEntitlementIds.isEmpty()) { SubscriptionStatus.Inactive } else { SubscriptionStatus.Active( activeEntitlementIds.map { Entitlement(id = it) }.toSet(), ) } } ``` `Entitlement` requires only an `id`; the remaining fields have defaults. > **Warning:** Do not leave the status at `SubscriptionStatus.Unknown` once you know the answer. Gated paywalls > and your own UI both branch on it. ## Consumables on Android Play Billing requires consuming a purchase before the same product can be bought again: ```kotlin val token = Superwall.consume(purchaseToken) ``` This is an **Android** operation. On iOS it echoes the token back unchanged, so it is safe to call from shared code without a platform check. ## Restoring `Superwall.restorePurchases()` routes through your controller's `restorePurchases()` when one is configured, and through the native SDK otherwise: ```kotlin when (val result = Superwall.restorePurchases()) { is RestorationResult.Restored -> println("Restored") is RestorationResult.Failed -> println("Failed: ${result.error}") } ``` Failure stays in the return type and does not throw. ## Observing purchases you did not make If you want Superwall to see transactions that happen outside of a paywall without taking over purchasing entirely, skip the controller and set an option instead: ```kotlin Superwall.configure( apiKey = "pk_your_api_key", options = SuperwallOptions(shouldObservePurchases = true), ) ``` # 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 Handle Superwall deep links for paywall previews and web checkout redemption. > **Warning:** **Beta**The KMP SDK is in beta and its API may change between releases. Superwall uses deep links for two things: previewing paywalls on a real device, and redeeming web checkout codes. Both flow through one call. ## Pass the URL to Superwall ```kotlin val handled = Superwall.handleDeepLink(url) ``` It returns whether the SDK recognized and handled the link, so you can fall through to your own routing when it did not: ```kotlin fun onDeepLink(url: String) { if (Superwall.handleDeepLink(url)) return myRouter.navigate(url) } ``` > **Note:** `handleDeepLink` is **guard-exempt**, so you can call it before `Superwall.configure` without hitting `SuperwallError.NotConfigured`. That is deliberate: cold-starting from a deep link is its primary use, and it would be useless if you had to sequence it behind configuration.It takes a `String`, not a platform URL type, so it is callable from `commonMain`. ## Wiring it up per platform The SDK takes a plain `String`, so the only platform-specific part is getting the URL from the OS to your shared code. **Android**, from the activity that receives the intent: ```kotlin // androidMain override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) intent?.data?.let { Superwall.handleDeepLink(it.toString()) } } override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) intent.data?.let { Superwall.handleDeepLink(it.toString()) } } ``` Declare your intent filter in `AndroidManifest.xml` as you would for any deep link. **iOS**, from your `App` or `AppDelegate`: ```swift // SwiftUI .onOpenURL { url in Superwall.shared.handleDeepLink(url: url.absoluteString) } ``` > **Note:** Register your URL scheme with the OS as usual: an `intent-filter` on Android, a URL type in your > Xcode target on iOS. Superwall does not do that part for you. ## Web checkout redemption When a user buys on the web and returns to your app, the redemption arrives as a deep link. Observe the outcome through the delegate: ```kotlin import com.superwall.sdk.kmp.models.redemption.RedemptionResult class MyDelegate : SuperwallDelegate { override fun willRedeemLink() { showSpinner() } override fun didRedeemLink(result: RedemptionResult) { hideSpinner() when (result) { is RedemptionResult.Success -> unlock(result.redemptionInfo) is RedemptionResult.Error -> showError(result.error) is RedemptionResult.ExpiredCode -> showExpired(result.info) is RedemptionResult.InvalidCode -> showInvalid() is RedemptionResult.ExpiredSubscription -> showExpiredSubscription() } } } ``` Every case carries the `code` that was redeemed. > **Warning:** Like every delegate callback, `willRedeemLink` and `didRedeemLink` are not guaranteed to arrive on the main thread. The spinner calls above need a main-thread hop. See [Platform differences](/docs/kmp/guides/platform-differences#delegate-threading). ## Superwall app links `SuperwallDelegate.handleSuperwallDeepLink` reports links of the form `yoursubdomain.superwall.app/app-link/...`, broken into path components and query parameters: ```kotlin override fun handleSuperwallDeepLink( fullURL: String, pathComponents: List, queryParameters: Map, ) { // Route based on pathComponents } ``` > **Warning:** **This hook is iOS only.** `superwall-android` has no equivalent delegate method, so it is never invoked on Android.`Superwall.handleDeepLink(url)` itself works on **both** platforms. Only this structured callback is missing. On Android, parse the URL yourself in the activity that receives it. ## Paywall previews Deep links are also how you preview a paywall on a real device from the dashboard. Point the link at your app and pass it to `handleDeepLink`. > **Note:** On **Android**, previews need the SDK's debug activities declared in your own manifest. The KMP library declares the paywall activity but not the debug ones. See [Platform differences](/docs/kmp/guides/platform-differences#in-app-paywall-previews-on-android) for the snippet. This path is not yet verified end-to-end on KMP. # 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 # Platform Differences Where Android and iOS behavior is not one-to-one in the KMP SDK. > **Warning:** **Beta**The KMP SDK is in beta and its API may change between releases. The KMP SDK's public API is identical on both platforms: one signature, no platform types, no `expect`/`actual` of your own. But it wraps two different native SDKs, and in a handful of places they do not offer the same thing. This page is the complete list. Everything not mentioned here behaves the same on Android and iOS. ## APIs that differ | API | Behavior | | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `SuperwallDelegate.handleSuperwallDeepLink` | **iOS only.** `superwall-android` has no equivalent delegate hook, so this is never invoked on Android. | | `Superwall.consume(purchaseToken)` | **Android.** Consumes a Play Billing purchase so it can be bought again. On iOS it echoes the token back unchanged. | | `IntegrationAttribute.FIREBASE_INSTALLATION_ID` | **iOS only.** `superwall-android` has no counterpart; setting it on Android is skipped and logs a warning. Every other `IntegrationAttribute` works on both. | That is the whole list of behavioral gaps. Notably, customer info is **not** on it. See [below](#what-is-not-a-difference). ## Options that only apply to one platform Setting one of these on the other platform is harmless. It is ignored. **Android only** | Option | What it does | | --------------------------------------------- | ----------------------------------------------------------- | | `SuperwallOptions.passIdentifiersToPlayStore` | Sends the raw `appUserId` to Play instead of a SHA-256 hash | | `SuperwallOptions.useMockReviews` | Enables mock review functionality | | `PaywallOptions.preloadDeviceOverrides` | Per-device-tier overrides for `shouldPreload` | | `PaywallOptions.onBackPressed` | Callback for the hardware back button while a paywall shows | **iOS only** | Option | What it does | | ------------------------------------------------------- | ------------------------------------------------------- | | `SuperwallOptions.shouldBypassAppTransactionCheck` | Skips the app transaction check on launch | | `SuperwallOptions.maxConfigRetryCount` | Retry attempts for fetching configuration (default `6`) | | `PaywallOptions.shouldShowWebRestorationAlert` | Offers web restoration after a failed restore | | `PaywallOptions.shouldShowWebPurchaseConfirmationAlert` | Confirms a successful web checkout purchase | > **Note:** `PaywallOptions.transactionBackgroundView` works on **both** platforms, despite its KDoc saying > "iOS only". `superwall-android` has the same option, and the KMP mapper wires it. `SPINNER` maps > to the native spinner and `NONE` maps to the native `null` ("show nothing"). ## Delegate threading This is the difference most likely to cause problems, because it is a runtime behavior rather than a missing method. > **Warning:** `SuperwallDelegate` callbacks are **not** guaranteed to arrive on any particular thread. Which thread a given callback lands on depends on the callback and the platform, and it may change between releases, so do not build on the current behavior.Treat every delegate callback as if it could arrive on a background thread:- Do not touch UI from a callback directly. Hop to the main thread yourself. > - Keep implementations thread-safe. Do not assume callbacks are serialized against each other. > - Keep them short. A callback runs before the SDK continues, so blocking in one blocks the SDK, and on whatever thread that happens to be.```kotlin > class MyDelegate(private val scope: CoroutineScope) : SuperwallDelegate { > override fun handleSuperwallEvent(eventInfo: SuperwallEventInfo) { > // Forwarding to an analytics SDK is fine from any thread. > analytics.track(eventInfo.eventType.name) > > // Anything that touches UI needs to be dispatched to main. > scope.launch(Dispatchers.Main) { updateMyUi() } > } > } > ``` > **Note:** Collecting `Superwall.subscriptionStatusFlow` is usually the easier path when you want subscription > changes to drive UI, but it is a plain `StateFlow`, so it delivers on *your* collector's context, > not on main. Collect it from a main-dispatched scope (`collectAsState`, `viewModelScope`, > `lifecycleScope`) and you are safe; collect it on `Dispatchers.IO` and you are not. > **Note:** `PaywallPresentationHandler` closures and the `register` `feature` closure are a different story: > those *are* delivered on the main thread on both platforms, deliberately, because they gate UI. ## Install differences The two platforms do not take the same amount of setup. See [Install the SDK](/docs/kmp/quickstart/install) for the detail. | | Android | iOS | | ------------------------ | ------------------------------------------------------------------------------------ | ----------------------------------------------------------------- | | Steps | One Gradle dependency | Gradle dependency **plus** the `SuperwallKMPBridge` Swift package | | Manifest / project edits | None. The library manifest declares the paywall activity and the startup initializer | Kotlin framework must be exported with `isStatic = true` | | Native SDK | `superwall-android` 2.8.0, transitively | SuperwallKit 4.16.1, pinned exactly by the bridge | | Minimum | `minSdk` 26 | iOS 14 | ## In-app paywall previews on Android > **Warning:** The KMP library manifest declares `SuperwallPaywallActivity`, which is what paywall presentation needs. It does **not** declare the debug activities that the standalone Android SDK's [in-app paywall previews](/docs/android/quickstart/in-app-paywall-previews) rely on, and neither does `superwall-android`.If you need previews on Android, declare them in your own `AndroidManifest.xml`:```xml > > > > ```This path is not yet verified end-to-end on KMP. If you try it, we would like to hear how it goes, so please [open an issue](https://github.com/superwall/Superwall-KMP/issues). ## Getting the right API key Your Android app and your iOS app are separate apps in the Superwall dashboard, and each one has its own Public API Key. In a KMP project, though, `Superwall.configure` is usually called once, from shared code. That single call site needs to end up with the Android key when the app runs on Android and the iOS key when it runs on iOS. One way to do that is Kotlin's `expect`/`actual`: ```kotlin // commonMain expect val superwallApiKey: String // androidMain actual val superwallApiKey: String = "pk_your_android_key" // iosMain actual val superwallApiKey: String = "pk_your_ios_key" // commonMain: one call site, correct key on each platform Superwall.configure(apiKey = superwallApiKey) ``` This is the same pattern the [sample app](https://github.com/superwall/Superwall-KMP/blob/main/sample/shared/src/commonMain/kotlin/com/superwall/sdk/kmp/sample/ApiKey.kt) uses. Passing the key in from each platform's entry point works just as well; use whatever your project already does for per-platform values. # 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 Superwall Delegate Observe the paywall lifecycle and SDK events from shared Kotlin code. > **Warning:** **Beta**The KMP SDK is in beta and its API may change between releases. `SuperwallDelegate` is how you observe what the SDK is doing: paywalls opening and closing, subscription status changing, events being tracked, links being redeemed. Every method has a default no-op implementation, so override only the ones you need. ## Setting the delegate ```kotlin import com.superwall.sdk.kmp.Superwall import com.superwall.sdk.kmp.SuperwallDelegate import com.superwall.sdk.kmp.models.paywall.PaywallInfo class MyDelegate : SuperwallDelegate { override fun didPresentPaywall(paywallInfo: PaywallInfo) { println("Presented ${paywallInfo.name}") } override fun didDismissPaywall(paywallInfo: PaywallInfo) { println("Dismissed ${paywallInfo.name}") } } Superwall.delegate = MyDelegate() ``` > **Note:** `delegate` is one of the few members you can set **before** `configure`. The value is stored > immediately and installed into the native SDK when configuration happens, so you will not miss > early events. Setting it to `null` clears it. ## What you can observe **Paywall lifecycle** ```kotlin override fun willPresentPaywall(paywallInfo: PaywallInfo) {} override fun didPresentPaywall(paywallInfo: PaywallInfo) {} override fun willDismissPaywall(paywallInfo: PaywallInfo) {} override fun didDismissPaywall(paywallInfo: PaywallInfo) {} ``` **Paywall interactions** ```kotlin override fun handleCustomPaywallAction(name: String) {} override fun paywallWillOpenURL(url: String) {} override fun paywallWillOpenDeepLink(url: String) {} ``` **State changes** ```kotlin override fun subscriptionStatusDidChange(from: SubscriptionStatus, to: SubscriptionStatus) {} override fun customerInfoDidChange(from: CustomerInfo, to: CustomerInfo) {} override fun userAttributesDidChange(newAttributes: Map) {} ``` **Analytics and logging** ```kotlin override fun handleSuperwallEvent(eventInfo: SuperwallEventInfo) {} override fun handleLog( level: LogLevel, scope: LogScope, message: String?, info: Map?, error: String?, ) {} ``` **Web checkout redemption** ```kotlin override fun willRedeemLink() {} override fun didRedeemLink(result: RedemptionResult) {} ``` ## Threading > **Warning:** Delegate callbacks are **not** guaranteed to arrive on any particular thread. Which thread a given callback lands on depends on the callback and the platform, and it may change between releases.Treat every callback as if it could arrive on a background thread:1) **Do not touch UI directly.** Hop to the main thread yourself. > 2) **Be thread-safe.** Do not assume callbacks are serialized against each other. > 3) **Be quick.** A callback runs before the SDK continues, so blocking in one blocks the SDK. If you need UI work from a callback, hop yourself. Give the delegate a scope to launch on: ```kotlin class MyDelegate(private val scope: CoroutineScope) : SuperwallDelegate { override fun subscriptionStatusDidChange(from: SubscriptionStatus, to: SubscriptionStatus) { scope.launch(Dispatchers.Main) { updateUi(to) } } } ``` Or skip the delegate for that case entirely and collect [`subscriptionStatusFlow`](/docs/kmp/quickstart/tracking-subscription-state) from a main-dispatched scope, which keeps the threading question in one place. ## Platform gap `handleSuperwallDeepLink` is **iOS only**. `superwall-android` has no equivalent delegate hook, so it is never invoked on Android. See [Platform differences](/docs/kmp/guides/platform-differences). ```kotlin // iOS only override fun handleSuperwallDeepLink( fullURL: String, pathComponents: List, queryParameters: Map, ) {} ``` ## The delegate and the flows Setting or clearing the delegate never uninstalls the SDK's own internal delegate, so `subscriptionStatusFlow` and `customerInfoFlow` keep working whether or not you have one set. Use whichever fits: * **Delegate** when you want the full event firehose, or the paywall lifecycle. * **Flows** when you want subscription state to drive UI, and you would rather not think about threads. # 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 # Vibe Coding How to Vibe Code using the knowledge of the Superwall Docs ## Overview We've built a few tools to help you Vibe Code using the knowledge of the Superwall Docs, access your Superwall account, and more right in your favorite AI tools: * [Superwall AI](#superwall-ai): The assistant built into the dashboard, these docs, and superwall.com. Ask about Superwall, your account, or your data, with nothing to set up. Start here. * [Superwall Agents](/docs/agents): An AI workspace for analyzing Superwall data exposed by connected tooling, reviewing experiments, working with available files, scheduling recurring reports, and connecting webhooks. * [Superwall Skill (Recommended for coding agents)](#superwall-skill-recommended-for-coding-agents): Gives AI agents live docs, API access, and step-by-step SDK integration guides. If you are working in an external coding agent, pick this one. * [Superwall CLI](#superwall-cli): Set up the SDK, manage your account, and query analytics from the terminal. Also the fastest way to integrate: `npx superwall integrate`. * [Superwall MCP](#superwall-mcp): Expose your Superwall account (projects, paywalls, campaigns) to work with AI tools. * [Editor AI](#editor-ai): Build and refine a paywall directly inside the visual editor using AI Chat or an external MCP-compatible agent. And right here in the Superwall Docs: * [Docs Links](#docs-links) * [LLMs.txt](#llmstxt) ## Superwall AI [Superwall AI](/docs/dashboard/superwall-ai) is the assistant in the bottom right 💬 of these docs, superwall.com, and the dashboard, and behind the **Ask AI** button in the docs search. It's an agent: it reads the docs and SDK source before answering, links what it used, and holds a conversation so you can follow up. In the dashboard it can also work with your account and analytics data. See the [Superwall AI guide](/docs/dashboard/superwall-ai). ## Superwall Skill (Recommended for Coding Agents) The [Superwall Skill](/docs/dashboard/guides/superwall-skill) is the best way to give external AI coding agents full context on Superwall. It bundles live documentation, API access, dashboard links, and guided SDK integration flows for every platform, all in one install. If you are already working in Claude Code, Cursor, Codex, or another coding agent, pick this one. ```bash npx superwall skills ``` Once installed, your agent can look up any Superwall doc on demand, call the API to inspect your projects and applications, and walk through a complete SDK integration step by step. It supports iOS, Android, Flutter, and Expo out of the box, and also includes the `superwall-editor` skill for editing live paywalls and the `wwdc` skill for WWDC session lookup. If you're only going to set up one tool, this is the one to use. See the full [Superwall Skill guide](/docs/dashboard/guides/superwall-skill) for details. ## Superwall CLI The [Superwall CLI](/docs/dashboard/guides/superwall-cli) puts your whole Superwall account in the terminal, for you and your coding agent: SDK setup, products, campaigns, paywalls, SQL analytics, and the Superwall and App Store Connect APIs. ```bash npx superwall integrate # sign up & integrate the SDK in one command ``` `integrate` detects your app, connects it to your Superwall account (creating one from the terminal if needed), and runs the Claude Code or Codex on your machine to install and configure the SDK. See the full [Superwall CLI guide](/docs/dashboard/guides/superwall-cli) for every command. ## Superwall MCP The Superwall MCP connects AI tools to your **Superwall account**, letting agents create and manage projects, paywalls, campaigns, products, entitlements, and webhooks directly. Instead of switching to the dashboard, your AI assistant can set everything up for you. If you also want live docs access and guided SDK integration help, use the [Superwall Skill](/docs/dashboard/guides/superwall-skill). The MCP is focused on account and resource management. See the full [Superwall MCP guide](/docs/dashboard/guides/superwall-mcp) for installation, a step-by-step quick setup, and the complete tool reference. ## Editor AI The paywall editor has AI tools for working on the paywall currently open in your browser: * [AI Chat Builder](/docs/dashboard/dashboard-creating-paywalls/paywall-editor-ai-chat): Open **AI Chat** in the editor sidebar, describe what you want changed, attach reference images, and let Superwall make draft edits for you. * [Editor MCP](/docs/dashboard/dashboard-creating-paywalls/paywall-editor-mcp): Connect Claude Code, Codex, Cursor, or another MCP-compatible agent to the live editor with a pairing code. Use Editor AI when the task is about changing the design, layout, products, variables, tap behaviors, navigation, or localization of the current paywall. Use the account-level [Superwall MCP](/docs/dashboard/guides/superwall-mcp) when the task is about creating or managing dashboard resources such as projects, applications, campaigns, products, entitlements, or webhooks. ## Docs Links At the top of each page of the Superwall Docs (including this one!): * **Copy Markdown**: to copy the page in Markdown format. Also in the **Open** dropdown menu, you can access these options: * **View as Markdown**: to view the page in Markdown format * **Open in ChatGPT**, **Open in Claude**: to open the page in the respective AI tool and add the page as context for your conversation You can also add `.md` to the end of any docs page URL to open that page as Markdown. For example, `https://superwall.com/docs/ios/quickstart/install.md`. ## LLMs.txt The Superwall Docs website has `llms.txt` and `llms-full.txt` files, in total and for each SDK, that you can use to add context to your LLMs. `llms.txt` is a summary of the docs with links to each page. `llms-full.txt` is the full text of all of the docs. | SDK | Summary | Full Text | | ------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | All | [`llms.txt`](https://superwall.com/docs/llms.txt) | [`llms-full.txt`](https://superwall.com/docs/llms-full.txt) | | Dashboard | [`dashboard/llms.txt`](https://superwall.com/docs/dashboard/llms.txt) | [`dashboard/llms-full.txt`](https://superwall.com/docs/dashboard/llms-full.txt) | | Agents | [`agents/llms.txt`](https://superwall.com/docs/agents/llms.txt) | [`agents/llms-full.txt`](https://superwall.com/docs/agents/llms-full.txt) | | iOS | [`ios/llms.txt`](https://superwall.com/docs/ios/llms.txt) | [`ios/llms-full.txt`](https://superwall.com/docs/ios/llms-full.txt) | | Android | [`android/llms.txt`](https://superwall.com/docs/android/llms.txt) | [`android/llms-full.txt`](https://superwall.com/docs/android/llms-full.txt) | | Flutter | [`flutter/llms.txt`](https://superwall.com/docs/flutter/llms.txt) | [`flutter/llms-full.txt`](https://superwall.com/docs/flutter/llms-full.txt) | | Expo | [`expo/llms.txt`](https://superwall.com/docs/expo/llms.txt) | [`expo/llms-full.txt`](https://superwall.com/docs/expo/llms-full.txt) | | KMP (Beta) | [`kmp/llms.txt`](https://superwall.com/docs/kmp/llms.txt) | [`kmp/llms-full.txt`](https://superwall.com/docs/kmp/llms-full.txt) | | Unity (Beta) | [`unity/llms.txt`](https://superwall.com/docs/unity/llms.txt) | [`unity/llms-full.txt`](https://superwall.com/docs/unity/llms-full.txt) | | Web (Beta) | [`web/llms.txt`](https://superwall.com/docs/web/llms.txt) | [`web/llms-full.txt`](https://superwall.com/docs/web/llms-full.txt) | | React Native (Deprecated) | [`react-native/llms.txt`](https://superwall.com/docs/react-native/llms.txt) | [`react-native/llms-full.txt`](https://superwall.com/docs/react-native/llms-full.txt) | | Integrations | [`integrations/llms.txt`](https://superwall.com/docs/integrations/llms.txt) | [`integrations/llms-full.txt`](https://superwall.com/docs/integrations/llms-full.txt) | | Web Checkout | [`web-checkout/llms.txt`](https://superwall.com/docs/web-checkout/llms.txt) | [`web-checkout/llms-full.txt`](https://superwall.com/docs/web-checkout/llms-full.txt) | To minimize token use, we recommend using the files specific to your SDK. # 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 # Welcome Welcome to the Superwall KMP SDK documentation > **Warning:** **Beta**The KMP SDK is in beta. The API may change between releases, and these docs describe behavior that is still settling. If you hit something that does not match what you see, please [open an issue](https://github.com/superwall/Superwall-KMP/issues). The Superwall KMP SDK brings paywalls, placements, and entitlements to Kotlin Multiplatform. It wraps the native Superwall SDKs (`superwall-android` on Android, SuperwallKit on iOS) behind a single API that lives entirely in `commonMain`. This means there's no `expect`/`actual` of your own, no platform types in your shared code, and an identical `configure` signature on both platforms. There is no `Context` parameter on Android. ```kotlin import com.superwall.sdk.kmp.Superwall Superwall.configure(apiKey = "pk_your_api_key") Superwall.register(placement = "campaign_trigger") { launchTheFeature() } ``` ## Platform support | Platform | Support | Wraps | | -------- | --------------------------------------------------- | ------------------------------------------- | | Android | `minSdk 26` | `com.superwall.sdk:superwall-android` 2.8.0 | | iOS | iOS 14+ (`iosArm64`, `iosSimulatorArm64`, `iosX64`) | SuperwallKit iOS 4.16.1, pinned exactly | Those are the only two targets. There are no JVM, JS, desktop, or watchOS artifacts since the SDK is a wrapper over two native SDKs. > **Note:** Android's `minSdk` here is **26**, higher than the standalone Android SDK's 23. If you are adding > KMP to an existing project, check your `minSdk` before you start. ## How it fits together Your shared Kotlin code calls one API. Underneath, each platform resolves to its own native SDK: * **Android** wraps `superwall-android` directly. One Gradle dependency pulls it in, and an `androidx.startup` initializer captures the `Application` for you. * **iOS** forwards through **SuperwallKMPBridge**, an `@objc` Swift bridging layer over SuperwallKit that flattens Swift-only constructs (enum associated values, structs, `async`) into something Kotlin can consume via cinterop. Your app supplies that bridge as a Swift package. ## Quick Links ## Quickstart /docs/kmp/quickstart/install Install, configure, and present your first paywall ## Platform differences /docs/kmp/guides/platform-differences Where Android and iOS behavior is not one-to-one ## Purchases /docs/kmp/guides/advanced-configuration Own your purchase logic with a `PurchaseController` ## Delegate /docs/kmp/guides/using-superwall-delegate Observe the paywall lifecycle and SDK events ## Example App https://github.com/superwall/Superwall-KMP/tree/main/sample A runnable Compose Multiplatform sample for Android and iOS ## Changelog /docs/kmp/changelog Release notes for the KMP SDK ## Feedback The KMP SDK is actively developed and we want to hear what is missing. If you have feedback on these docs, please leave a rating and message at the bottom of the page. For SDK bugs, [open an issue on GitHub](https://github.com/superwall/Superwall-KMP/issues). # 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 # Configure the SDK Configure Superwall in your shared Kotlin code. > **Warning:** **Beta**The KMP SDK is in beta and its API may change between releases. ## Configure Call `Superwall.configure` as early as possible in your app's lifecycle, from shared code: ```kotlin import com.superwall.sdk.kmp.Superwall Superwall.configure(apiKey = "pk_your_api_key") { result -> result.onFailure { println("Superwall configuration failed: $it") } } ``` The signature is identical on both platforms: The call is fire-and-forget. `completion` reports the real outcome. An invalid API key surfaces there, not as a thrown exception. > **Note:** Calling `configure` a second time is a no-op. The repeat call logs a warning through the delegate's > `handleLog`, does not re-install your options or purchase controller, and invokes its completion > with the first call's outcome. ## There is no pre-configure call queue This is important to understand before you write anything else. > **Warning:** Calls made before `configure` are **not** buffered and replayed. Almost every member throws `SuperwallError.NotConfigured` instead.The deliberate exemptions, which are safe to touch at any time:- `handleDeepLink`: deep-link cold start is its whole purpose > - `subscriptionStatusFlow` and `customerInfoFlow`: pre-seeded, common-owned flows > - `delegate`: stored immediately, installed natively at configure > - `isConfigured`, `isInitialized`, and `configurationStatus` Everything else (`register`, `identify`, `setUserAttributes`, `entitlements`, and the rest) needs configuration to have happened first. ## Ordering your calls Two supported ways to sequence work behind configuration. **Await it.** `configureAndAwait` is the suspending twin, and the sanctioned ordering tool. It resumes when the native SDK reports completion and throws `SuperwallError.ConfigurationFailed` on failure: ```kotlin suspend fun startSuperwall() { Superwall.configureAndAwait(apiKey = "pk_your_api_key") // Safe from here on. Superwall.identify(userId = "abc123") } ``` **Gate on the flag.** `Superwall.isConfigured` is readable at any time: ```kotlin if (Superwall.isConfigured) { Superwall.register(placement = "campaign_trigger") } ``` `Superwall.configurationStatus` gives you the fuller picture: `PENDING`, `CONFIGURED`, or `FAILED`. ## Options Pass `SuperwallOptions` to customize behavior. Every field has a default, so set only what you need: ```kotlin import com.superwall.sdk.kmp.models.options.PaywallOptions import com.superwall.sdk.kmp.models.options.SuperwallOptions Superwall.configure( apiKey = "pk_your_api_key", options = SuperwallOptions( paywalls = PaywallOptions( shouldPreload = false, isHapticFeedbackEnabled = false, ), ), ) ``` Frequently used `SuperwallOptions` fields: Some options only apply to one platform. `passIdentifiersToPlayStore` and `useMockReviews` are Android-only; `shouldBypassAppTransactionCheck` and `maxConfigRetryCount` are iOS-only. Setting one on the other platform is harmless. It is ignored. See [Platform differences](/docs/kmp/guides/platform-differences). > **Note:** Leave `networkEnvironment` alone unless the Superwall team has explicitly told you otherwise. ## Logging Set the log level at configure time, or change it later: ```kotlin import com.superwall.sdk.kmp.models.options.LogLevel Superwall.logLevel = LogLevel.WARN ``` Levels are `DEBUG`, `INFO`, `WARN`, `ERROR`, and `NONE`. > **Note:** On iOS, `LogLevel.NONE` maps to Swift's `.none`. If you are reading native logs or Swift docs > alongside these, do not mistake it for an absent optional. Next, [present your first paywall](/docs/kmp/quickstart/present-first-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 # Feature Gating Control access to premium features with Superwall placements. > **Warning:** **Beta**The KMP SDK is in beta and its API may change between releases. ## The idea `Superwall.register` lets you register a [placement](/docs/dashboard/dashboard-campaigns/campaigns-placements) to access a feature that may or may not be paywalled later in time. Whether the user can access that feature without paying is a dashboard decision, not a code decision. ```kotlin fun pressedWorkoutButton() { // Remotely decide if a paywall is shown, and whether // startWorkout() is a paid-only feature. Superwall.register(placement = "StartWorkout") { navigation.startWorkout() } } ``` Given how cheap `register` is, we strongly recommend registering **all core functionality**. That is what lets you change what is gated without shipping an app update. ## What happens when you register When you register a placement: 1. The SDK checks your campaigns for a matching audience filter. 2. If one matches and the user is not in a holdout, the assigned paywall is presented. 3. Once a user is assigned a paywall for an audience, they keep seeing that paywall until you remove it from the audience or reset assignments. 4. After the paywall closes, the SDK looks at the paywall's **Feature Gating** value, set in the paywall editor under **General → Feature Gating**: * **Non Gated**: the `feature` closure runs when the paywall is dismissed, whether they paid or not. * **Gated**: the `feature` closure runs only if the user is already paying, or begins paying. 5. If no paywall is configured for the placement, the feature runs immediately with no extra network calls. ## Gating with entitlements directly Sometimes you need to branch on subscription state rather than gate a call. Read it synchronously: ```kotlin import com.superwall.sdk.kmp.models.entitlements.SubscriptionStatus if (Superwall.subscriptionStatus.isActive) { showProContent() } else { showFreeContent() } ``` Or collect the flow to keep UI in sync. See [Tracking subscription state](/docs/kmp/quickstart/tracking-subscription-state). > **Note:** Prefer `register` with a `feature` closure over hand-rolled `if` checks where you can. The closure > keeps the decision on the dashboard; an `if` statement hard-codes it into the build. ## Inspecting entitlements `Superwall.entitlements` is an immutable snapshot: ```kotlin val entitlements = Superwall.entitlements entitlements.active // Set entitlements.inactive // Set entitlements.all // Set entitlements.web // Set, granted via web checkout ``` Each `Entitlement` carries its `id`, `productIds`, `store`, expiry and renewal dates, and whether it is a lifetime purchase. To resolve entitlements for specific products, use `getEntitlementsByProductIds`, which asks the native SDK on both platforms: ```kotlin val granted = Superwall.getEntitlementsByProductIds(setOf("pro_monthly", "pro_annual")) ``` ## Previewing the outcome To adjust UI *before* a placement fires (hiding an upgrade button for users who would never see a paywall, for example), ask what registering would do: ```kotlin val result = Superwall.getPresentationResult(placement = "StartWorkout") ``` This presents nothing. It just tells you what would happen. Next, [track subscription state](/docs/kmp/quickstart/tracking-subscription-state). # 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 # Install the SDK Add the Superwall KMP SDK to your Kotlin Multiplatform project. > **Warning:** **Beta**The KMP SDK is in beta and its API may change between releases. ## Requirements | Target | Requirement | | ------- | --------------------------------------------------- | | Android | `minSdk` 26, `compileSdk` 36 | | iOS | iOS 14+ (`iosArm64`, `iosSimulatorArm64`, `iosX64`) | | Kotlin | 2.3.10 | Android is the shorter path: one Gradle dependency and you are done. iOS needs a second step, because the Kotlin framework does not embed the Swift bridge binary it compiles against. ## Android Add the dependency to your shared module. Because the public API lives in `commonMain`, you can declare it there and both targets pick it up. ## Tab ```kotlin build.gradle.kts kotlin { sourceSets { commonMain.dependencies { implementation("com.superwall.sdk:superwall-kmp:0.1.1") } } } ``` ## Tab ```toml libs.versions.toml [versions] superwall-kmp = "0.1.1" [libraries] superwall-kmp = { module = "com.superwall.sdk:superwall-kmp", version.ref = "superwall-kmp" } # And in your shared module's build.gradle.kts # commonMain.dependencies { implementation(libs.superwall.kmp) } ``` `superwall-android` comes along transitively, so do not add it yourself. > **Note:** **You do not need to edit `AndroidManifest.xml`.** This is the main way KMP install differs from the [standalone Android SDK](/docs/android/quickstart/install), whose docs ask you to declare the paywall activity and permissions by hand.The KMP library manifest already declares `SuperwallPaywallActivity`, and `superwall-android` declares the `INTERNET`, `ACCESS_NETWORK_STATE`, and `POST_NOTIFICATIONS` permissions. (`com.android.vending.BILLING` arrives from the Play Billing library.) Manifest merging folds all of it into your app.**Migrating from the standalone Android SDK?** Remove the `SuperwallPaywallActivity` declaration from your own manifest first. Keeping it will fail the manifest merger, because the KMP library declares the same activity with a different theme (`Theme.AppCompat.NoActionBar`). If you need your own theme, override it with `tools:replace="android:theme"` rather than declaring the activity twice. ### No `Context`, and what to do if that breaks `Superwall.configure` takes no `Context` on Android. An `androidx.startup` initializer in the library manifest captures the `Application` before any of your code runs, which is what lets the `commonMain` signature stay platform-free. If your app strips the startup provider (some apps remove `InitializationProvider` deliberately, and some shrinkers remove it by accident), that capture never happens and `configure()` fails with `SuperwallError.NotInitialized`. The fallback is an Android-only extension function: ```kotlin // androidMain: only needed if the androidx.startup provider was removed import com.superwall.sdk.kmp.androidSetup class MyApplication : Application() { override fun onCreate() { super.onCreate() Superwall.androidSetup(this) } } ``` It is idempotent, so calling it defensively alongside a working initializer is harmless. ## iOS iOS is two steps. Miss the second one and the app fails to link. ### 1\. Export the Kotlin framework as static In your shared module, the framework **must** be static: ```kotlin // shared/build.gradle.kts kotlin { listOf(iosArm64(), iosSimulatorArm64(), iosX64()).forEach { it.binaries.framework { baseName = "Shared" isStatic = true // required } } } ``` > **Warning:** `isStatic = true` is required, not optional. The Kotlin framework compiles against the bridge's Objective-C headers only (compile-only cinterop) and never embeds the bridge binary, so a dynamic framework has nothing to resolve those symbols against at link time. ### 2\. Add the SuperwallKMPBridge Swift package In Xcode: **File → Add Package Dependencies…**, then paste the repository URL: ``` https://github.com/superwall/Superwall-KMP ``` Add the **`SuperwallKMPBridge`** product to your app target. ![Xcode](/docs/images/kmp/spm-add-bridge-product.jpg) > **Note:** Xcode defaults the Dependency Rule to **Up to Next Major Version**, as shown. While the SDK is in > beta, pin **Exact Version** instead. The bridge binary and the Kotlin klib ship from the same tag > and are one release unit, so letting SPM drift ahead of the version your shared module was built > against is what breaks the link step. > **Warning:** **Do not add SuperwallKit separately.**The bridge package pins SuperwallKit iOS to exactly `4.16.1` and pulls it in transitively. The pinned binary and the headers the Kotlin side was compiled against are one release unit. Adding SuperwallKit yourself invites a version conflict that SPM cannot resolve. > **Warning:** **A missing bridge is a build-time failure, not a runtime one.** Because the Kotlin side uses compile-only cinterop, forgetting this step surfaces as undefined-symbol link errors when Xcode builds your app, not as a crash on launch or a paywall that silently fails to present. If you see linker errors mentioning bridge symbols, this step is what is missing. ## Get your API key You need your **Public API Key** from the Superwall dashboard, under your app's settings. It is safe to ship in client code. Your Android and iOS apps are separate apps in the dashboard, and each has its own key. Since your `configure` call lives in shared code, it needs to receive the Android key on Android and the iOS key on iOS. [Getting the right API key](/docs/kmp/guides/platform-differences#getting-the-right-api-key) shows a pattern for this. ## Verify the install Build both targets before you write any integration code. On iOS in particular, a successful build is the signal that step 2 landed: ```bash ./gradlew :shared:build ``` Then build the iOS app from Xcode. **And you're done!** Now you're ready to [configure the SDK](/docs/kmp/quickstart/configure) 👇 # Superwall: Subscription Infrastructure for iOS, Android, and Web Subscription infrastructure — entitlements, purchase APIs, webhook delivery, and direct SQL access to subscription data — for iOS, Android, and Web. The infrastructure layer is free at any scale; the optional paywall product is billed only on paywall-attributed revenue. ## Pricing - **Infrastructure: free at any scale, every plan.** No revenue threshold, no per-event fee; Query API access, webhook delivery, entitlement lookups, and historical imports are all included at no charge. - **Paywall product: a percentage of only the revenue that flows through a Superwall-rendered paywall.** Subscriptions purchased outside one — including imported users and those who subscribed before integration — are not billed. Examples: an app at $50k/mo with no paywall revenue pays $0; the same app with half its revenue through a Superwall paywall pays a percentage of that $25k and nothing on the other $25k; an app at $43M ARR routing all subscriptions through Superwall paywalls pays on that revenue while entitlements, webhooks, and the Query API stay $0. ## Scale $1.5B+ annual subscription revenue across 10,000+ apps. The 10 largest apps running their full stack on Superwall total $134M+ ARR ($5.7M–$43.7M each). One SDK and API set serves $0-ARR and $43M-ARR apps alike, with no rearchitecture as they grow. ## Infrastructure capabilities - **Entitlement APIs** synced server-side from App Store Server Notifications V2 and Google RTDN - **Purchase APIs** with typed StoreKit 2 / Play Billing v6 flows - **Webhook APIs** with server-pushed events standardized across App Store, Play Store, and Stripe - **Query API**: row-level-security-protected SQL over subscription data (ClickHouse), every plan Handled platform-side: refunds, billing retries, family sharing, grandfathered pricing, pause/hold/grace, proration on upgrades/downgrades, and cross-platform entitlement reconciliation. ## Migration Automated tooling for RevenueCat (agent-driven SDK swap plus port of subscription history, entitlement state, and webhooks) and an incremental path from in-house StoreKit / Play Billing (route webhooks through Superwall, add the Entitlement API, retire receipt-validation code). ## Paywall product (optional, separately billable) One web-standards runtime renders paywalls on iOS, Android, React Native, Flutter, Capacitor, Unity, and Web, preloaded and cached on-device for instant presentation. Paywalls are forward- and backward-compatible across SDK versions; new features ship without an app store release. ## Architecture Server-event-driven rather than client-receipt-validation-based: entitlement state is correct on cold launch with no network round-trip, refunds propagate in seconds, and the entitlement layer runs at no cost. ## Docs * Migrate from RevenueCat: https://superwall.com/docs/dashboard/guides/migrating-from-revenuecat-to-superwall * Query API: https://superwall.com/docs/dashboard/guides/query-clickhouse * Webhooks: https://superwall.com/docs/integrations/webhooks * Pricing: https://superwall.com/pricing # Present your first paywall Register a placement and present a Superwall paywall from shared code. > **Warning:** **Beta**The KMP SDK is in beta and its API may change between releases. ## Register a placement Paywalls are presented by registering a **placement**. You do not tell the SDK to show a paywall. You tell it a placement occurred, and your campaign on the dashboard decides what happens. ```kotlin Superwall.register(placement = "campaign_trigger") ``` That one line is a complete integration. Whether a paywall appears, which one, and to whom, is all configured on the dashboard without shipping an app update. > **Note:** The placement name must match one you have added to a campaign on the > [dashboard](https://superwall.com/dashboard). A name that is not in any campaign resolves to > `PaywallSkippedReason.PlacementNotFound`. ## Gate a feature behind it Pass a `feature` closure to run code that should only happen if the user is entitled to it: ```kotlin Superwall.register(placement = "campaign_trigger") { launchTheFeature() } ``` When the closure runs depends on the paywall's feature-gating behavior: immediately when no paywall shows, or after a purchase or restore when the placement is gated. [Feature gating](/docs/kmp/quickstart/feature-gating) covers the rules. ## Pass parameters `params` are usable in audience filters and on the paywall itself: ```kotlin Superwall.register( placement = "campaign_trigger", params = mapOf( "source" to "onboarding", "isTrialEligible" to true, ), ) ``` Values may be `String`, `Boolean`, `Long`, `Double`, `List`, `Map`, or `Set`. Anything else is stringified. ## Observe what happened A `PaywallPresentationHandler` reports on the presentation. Set only the closures you care about; unset ones are never invoked. ```kotlin import com.superwall.sdk.kmp.PaywallPresentationHandler import com.superwall.sdk.kmp.models.results.PaywallResult import com.superwall.sdk.kmp.models.results.PaywallSkippedReason val handler = PaywallPresentationHandler() handler.onPresent { info -> println("Presented ${info.name}") } handler.onDismiss { info, result -> when (result) { is PaywallResult.Purchased -> println("Purchased ${result.productId}") is PaywallResult.Restored -> println("Restored") is PaywallResult.Declined -> println("Declined") } } handler.onSkip { reason -> when (reason) { is PaywallSkippedReason.Holdout -> println("Holdout: ${reason.experiment.id}") is PaywallSkippedReason.NoAudienceMatch -> println("No audience match") is PaywallSkippedReason.PlacementNotFound -> println("Placement not found") } } handler.onError { error -> println("Paywall error: $error") } Superwall.register( placement = "campaign_trigger", handler = handler, ) { launchTheFeature() } ``` > **Note:** `onSkip` is not a failure path. A holdout or an unmatched audience filter means your campaign > worked as configured. The user was not meant to see a paywall. All handler closures and the `feature` closure are delivered on the **main thread**, on both platforms. You can touch UI from them directly. ### Custom callbacks `onCustomCallback` is the one closure that returns a value. A paywall can request an action from your app and branch on the result: ```kotlin import com.superwall.sdk.kmp.models.callbacks.CustomCallbackResult handler.onCustomCallback { callback -> when (callback.name) { "validate_email" -> { val email = callback.variables?.get("email") as? String if (isValidEmail(email)) { CustomCallbackResult.success(mapOf("validated" to true)) } else { CustomCallbackResult.failure(mapOf("error" to "Invalid email")) } } else -> CustomCallbackResult.failure() } } ``` It is a `suspend` closure, so you can do real work in it. When it is unset, the SDK responds with `CustomCallbackResult.failure()`. ## Check before you register `getPresentationResult` previews what registering *would* do without presenting anything, which is useful for adjusting UI ahead of time, like hiding an upgrade button for users who would not see a paywall: ```kotlin val result = Superwall.getPresentationResult(placement = "campaign_trigger") ``` ## Controlling the paywall A few members act on the presented paywall: ```kotlin Superwall.dismiss() // suspend; resumes once dismissed Superwall.isPaywallPresented // Boolean Superwall.latestPaywallInfo // PaywallInfo? Superwall.togglePaywallSpinner(isHidden = true) ``` ## Preloading Paywalls preload by default. To take over the timing, disable it and preload yourself: ```kotlin Superwall.configure( apiKey = "pk_your_api_key", options = SuperwallOptions(paywalls = PaywallOptions(shouldPreload = false)), ) // Later Superwall.preloadAllPaywalls() Superwall.preloadPaywalls(placementNames = setOf("campaign_trigger")) ``` Next, [manage your users](/docs/kmp/quickstart/user-management). # 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 # Tracking Subscription State Observe whether a user is on a paid plan from shared Kotlin code. > **Warning:** **Beta**The KMP SDK is in beta and its API may change between releases. Superwall tracks subscription state for you. But there are times you need to know directly whether a user is on a paid plan, either to show different UI or to unlock something without a placement. ## Read it synchronously ```kotlin import com.superwall.sdk.kmp.models.entitlements.SubscriptionStatus when (val status = Superwall.subscriptionStatus) { is SubscriptionStatus.Active -> showPro(status.entitlements) is SubscriptionStatus.Inactive -> showFree() is SubscriptionStatus.Unknown -> showLoading() } ``` `SubscriptionStatus` has three states: | State | Meaning | | -------------------------- | ------------------------------------------------------------ | | `Unknown` | Not yet determined, typically before configuration completes | | `Active(Set)` | The user has one or more active entitlements | | `Inactive` | The user has no active entitlements | There is also a convenience boolean: ```kotlin if (Superwall.subscriptionStatus.isActive) { /* ... */ } ``` > **Warning:** Treat `Unknown` as its own case, not as "not subscribed". Showing free-tier UI during `Unknown` > will flash the wrong state at paying users on cold start. ## Observe changes `subscriptionStatusFlow` is a `StateFlow`, so it always has a current value and emits on change: ```kotlin import kotlinx.coroutines.launch scope.launch { Superwall.subscriptionStatusFlow.collect { status -> when (status) { is SubscriptionStatus.Active -> showPro(status.entitlements) is SubscriptionStatus.Inactive -> showFree() is SubscriptionStatus.Unknown -> showLoading() } } } ``` > **Note:** **Two naming details to watch for if you are coming from the Android SDK.**The flow is `Superwall.subscriptionStatusFlow`. The plain `Superwall.subscriptionStatus` is a synchronous property, not a flow. On the Android SDK, `subscriptionStatus` *is* the flow\.And this flow is **guard-exempt**: you can collect it before `configure`, where it is seeded with `SubscriptionStatus.Unknown` and attached to the native source once configuration completes. You do not have to sequence collection behind configuration. > **Warning:** This is a plain `StateFlow`, which means it delivers on **your collector's context** and does not force emissions onto the main thread. Collect it from a main-dispatched scope (`collectAsState`, `viewModelScope`, `lifecycleScope`) and updating UI from the collector is safe. Collect it on `Dispatchers.IO` and it is not.The SDK's own KDoc says emissions arrive on the main thread; that is true of the common case, not a guarantee the flow enforces. ### With Compose Multiplatform ```kotlin @Composable fun ContentScreen() { val status by Superwall.subscriptionStatusFlow.collectAsState() when (val current = status) { is SubscriptionStatus.Active -> PremiumContent(current.entitlements) is SubscriptionStatus.Inactive -> FreeContent() is SubscriptionStatus.Unknown -> LoadingIndicator() } } ``` ## Setting it yourself If you configured with a [`PurchaseController`](/docs/kmp/guides/advanced-configuration), you own subscription state and must set it: ```kotlin Superwall.subscriptionStatus = SubscriptionStatus.Active(entitlements) ``` > **Note:** Only set this when you have a `PurchaseController`. Without one, Superwall manages the value and > writing to it will fight the SDK. ## Detailed purchase history `CustomerInfo` carries more than `SubscriptionStatus` does, including full transaction history that merges device and web purchases: ```kotlin val info = Superwall.getCustomerInfo() info.subscriptions // List info.nonSubscriptions // List info.entitlements // List info.userId // String ``` Each `SubscriptionTransaction` includes `productId`, `purchaseDate`, `expirationDate`, `willRenew`, `isActive`, `isInGracePeriod`, and more. Observe changes with `customerInfoFlow`: ```kotlin scope.launch { Superwall.customerInfoFlow.collect { info -> render(info) } } ``` > **Note:** Customer info works on **both** platforms. If you are reading the SDK's own KDoc, note that the > `@platform iOS` annotations on `customerInfoFlow` and `getCustomerInfo` are out of date. They > describe a limitation that no longer applies. On Android the flow is fed by the native > `customerInfoDidChange` delegate hook, and `getCustomerInfo()` calls straight through to > `superwall-android` 2.8.0. > **Note:** `customerInfoFlow` has no replay, so a new collector gets nothing until the next change. Use > `getCustomerInfo()` for the current value. ## Restoring purchases ```kotlin import com.superwall.sdk.kmp.models.results.RestorationResult when (val result = Superwall.restorePurchases()) { is RestorationResult.Restored -> println("Restored") is RestorationResult.Failed -> println("Restore failed: ${result.error}") } ``` Restoration failure stays in the return type and does not throw. `Restored` means the restore completed without errors, not that the user necessarily has an active subscription. # 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 # User Management Identify users and set attributes from shared Kotlin code. > **Warning:** **Beta**The KMP SDK is in beta and its API may change between releases. It is necessary to uniquely identify users to track their journey within Superwall. ## Anonymous users Superwall automatically generates a random user ID that persists until the user deletes or reinstalls your app. You do not have to do anything to get one. ```kotlin Superwall.userId // the generated alias, or your ID once identified Superwall.isLoggedIn // false until identify() is called ``` ## Identified users If you have your own user management system, call `identify` as soon as you have an ID, right after log in or sign up. This aliases your ID with the anonymous Superwall ID, which is what lets us load that user's assigned paywalls. ```kotlin // After retrieving a user's ID, e.g. from logging in or creating an account Superwall.identify(userId = user.id) // When the user signs out Superwall.reset() ``` `reset()` returns the user to a fresh random ID and clears on-device paywall assignments and stored data. ### Waiting for assignments If your users switch accounts often, or delete and reinstall frequently, you can make the SDK hold paywalls back until assignments have been restored from the server: ```kotlin import com.superwall.sdk.kmp.models.identity.IdentityOptions Superwall.identify( userId = user.id, options = IdentityOptions(restorePaywallAssignments = true), ) ``` > **Note:** This is an advanced option and defaults to `false`. Turning it on delays paywall presentation until > assignments arrive, so only reach for it when logging a user into an *existing* account. ## User attributes Attributes are usable in audience filters and can be templated onto paywalls. ```kotlin Superwall.setUserAttributes( mapOf( "firstName" to "Jack", "plan" to "trial", "workoutCount" to 12L, ), ) ``` Values may be `String`, `Boolean`, `Long`, `Double`, `List`, `Map`, or `Set`. Anything else is stringified. > **Warning:** **`setUserAttributes` merges rather than replaces.** Keys you pass are merged into the existing attributes, a `null` value **removes** that key, and keys you leave out are untouched.That asymmetry is deliberate, and it is why this is a method rather than a settable property: reading `Superwall.userAttributes` after setting will not give you back only what you set. ```kotlin // Remove a single attribute Superwall.setUserAttributes(mapOf("plan" to null)) // Read the current snapshot val attributes = Superwall.userAttributes ``` ## Third-party integration attributes To line Superwall up with your analytics and attribution providers, set integration attributes: ```kotlin import com.superwall.sdk.kmp.models.events.IntegrationAttribute Superwall.setIntegrationAttribute(IntegrationAttribute.AMPLITUDE_DEVICE_ID, "device-123") Superwall.setIntegrationAttributes( mapOf( IntegrationAttribute.AMPLITUDE_USER_ID to "user-abc", IntegrationAttribute.MIXPANEL_DISTINCT_ID to "distinct-xyz", ), ) // Passing null removes an attribute Superwall.setIntegrationAttribute(IntegrationAttribute.AMPLITUDE_USER_ID, null) ``` Read them back with `Superwall.integrationAttributes`. ## Device attributes The device attributes Superwall tracks are also available for audience filters: ```kotlin val deviceAttributes = Superwall.getDeviceAttributes() ``` ## Google Play account identifiers > **Note:** By default the SDK SHA-256 hashes your `userId` before forwarding it to Google Play. If you need the raw `appUserId` to appear in Play Console and downstream server events, set `passIdentifiersToPlayStore = true` when configuring:```kotlin > Superwall.configure( > apiKey = "pk_your_api_key", > options = SuperwallOptions(passIdentifiersToPlayStore = true), > ) > ```This option is **Android only** and is ignored on iOS. Make sure the value complies with [Google's policies](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setObfuscatedAccountId), and note that it must not contain personally identifiable information. ## Setting the locale Override the locale used to evaluate audience filters, or pass `null` to follow the device: ```kotlin Superwall.localeIdentifier = "en_GB" Superwall.localeIdentifier = null // back to the device locale ``` Next, [gate your features](/docs/kmp/quickstart/feature-gating).