# 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

# Customer Center

Give users a native screen to manage subscriptions, restore purchases, request refunds, and contact support.

The Customer Center is a self-service screen for managing purchases. Users can view their subscriptions and purchases, restore purchases, cancel or change a Google Play subscription, request a refund, manage a [web checkout](/docs/android/guides/web-checkout) subscription, and contact support. It requires SDK version 2.8.5 or later and runs on every API level the SDK supports.

## Presenting the Customer Center

Call `presentCustomerCenter()` from the main thread after configuring the SDK:

```kotlin
Superwall.instance.presentCustomerCenter(
    onDismiss = {
        // The Customer Center was dismissed.
    },
)
```

Only one Customer Center can be open at a time; presenting it while it's already open does nothing. Call `dismissCustomerCenter()` to dismiss it from your app. See [`presentCustomerCenter`](/docs/android/sdk-reference/Superwall#present-the-customer-center-285) for the full signature.

The Customer Center opens as its own full-screen activity on top of your current one, so it can't be embedded in your own layout or navigation graph. Its toolbar has a close button on the main screen and a back arrow on a subscription's detail screen. The system back gesture closes a subscription's detail screen first, then the Customer Center itself. It keeps its place across configuration changes such as rotation.

Present it while your app is in the foreground. Android can refuse to start an activity from the background without reporting an error, so if the Customer Center hasn't appeared within five seconds, the SDK gives up and logs an error. Your `onDismiss` handler still runs so you aren't left waiting, but the delegate's `customerCenterDidDismiss` is not called, since nothing opened.

## What users see

The SDK shows the **management screen** when the user has at least one subscription (active or expired), one-time purchase, or active entitlement, and the **no purchases screen** when they have none.

The management screen lists each subscription as a row that opens its own detail screen, alongside Restore purchases and account details. An active entitlement with no transaction behind it also opens a detail screen. One-time purchases appear as cards without one. Repeat renewals of the same subscription collapse into a single row.

Cancel subscription, Change plan, and Request a refund each act on one subscription, so they live on that subscription's detail screen rather than the management screen. Restore purchases is the reverse: it appears on the management and no purchases screens, never on a detail screen.

When the user comes back from Google Play or a web management page, the Customer Center reloads their purchases so a cancellation or plan change made there shows up right away.

## Customizing the Customer Center

By default, [`SuperwallOptions.customerCenter`](/docs/android/sdk-reference/SuperwallOptions#parameters) uses `CustomerCenterConfiguration.default`, which works with zero setup. The only row that needs configuration is **Contact support**, which stays hidden until you set a support email.

To customize it, set `customerCenter` before calling `configure`:

```kotlin
val options = SuperwallOptions().apply {
    customerCenter = CustomerCenterConfiguration(
        managementScreen = CustomerCenterConfiguration.Screen(
            paths = listOf(
                CustomerCenterConfiguration.Path.restore(),
                CustomerCenterConfiguration.Path.changePlan(),
                CustomerCenterConfiguration.Path.refund(),
                CustomerCenterConfiguration.Path.manageSubscription(
                    survey = CustomerCenterConfiguration.FeedbackSurvey.cancellation,
                ),
                CustomerCenterConfiguration.Path.url(
                    url = "https://mycompany.com/faq",
                    title = "FAQ",
                ),
                CustomerCenterConfiguration.Path.contactSupport(),
            ),
        ),
        noPurchasesScreen = CustomerCenterConfiguration.Screen(
            paths = listOf(CustomerCenterConfiguration.Path.restore()),
        ),
        support = CustomerCenterConfiguration.Support(email = "support@mycompany.com"),
    )
}

Superwall.configure(
    applicationContext = this,
    apiKey = "MY_API_KEY",
    options = options,
)
```

Pass a `configuration` to `presentCustomerCenter()` to override it for one presentation. See [`CustomerCenterConfiguration`](/docs/android/sdk-reference/CustomerCenterConfiguration) for every screen, path, survey, support, and appearance option.

> **Tip:** A configuration you supply **replaces** the default rather than merging with it, so the screen shows exactly what you list and nothing else. To keep the defaults and change one thing, copy the default:```kotlin
> options.customerCenter = CustomerCenterConfiguration.default.copy(
>     support = CustomerCenterConfiguration.Support(email = "support@mycompany.com"),
> )
> ```

The SDK logs a warning when a configuration renders but renders badly: two paths sharing an `id` on one screen, a URL path with no title (the row falls back to the URL's host), a survey with no title on any path other than `manageSubscription`, or an accent color that isn't valid hex.

### When each path appears

Paths only appear when they make sense for the user's purchases, so you don't have to gate any of this yourself:

| Path                   | Opens                                                                                                                                                   | Shows when                                                                                                                                                                      |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `restore()`            | A progress overlay, then a result dialog. If your delegate declines the restore, nothing appears.                                                       | Always, on the management and no purchases screens.                                                                                                                             |
| `manageSubscription()` | The subscription's page in Google Play, labeled "Cancel subscription". For a web subscription, your web management page, labeled "Manage subscription". | A Google Play subscription that's active and set to renew, or an active web subscription.                                                                                       |
| `changePlan()`         | The subscription's page in Google Play.                                                                                                                 | An active Google Play subscription, including one the user has cancelled that hasn't expired yet.                                                                               |
| `refund()`             | Google Play's order history, where the user submits the request.                                                                                        | A Google Play subscription that isn't in a free trial, already refunded, or free, and is still inside `windowMillis` if you set one. It still shows on an expired subscription. |
| `contactSupport()`     | A pre-filled support email.                                                                                                                             | `support.email` is set.                                                                                                                                                         |
| `url()`                | The URL, in a Custom Tab by default.                                                                                                                    | Always.                                                                                                                                                                         |
| `custom()`             | Nothing. Handle it in your [delegate](#reacting-to-what-users-do).                                                                                      | Always.                                                                                                                                                                         |

`changePlan(productIds)` accepts `productIds`, but it has no effect on Android: the path always opens the subscription's page in Google Play.

`contactSupport()`, `url()`, and `custom()` don't depend on a purchase, so they appear on every detail screen as well as the main screens. When a user taps one there, the delegate receives that subscription as `purchase`.

When no path applies to a purchase, its detail screen shows a line of text instead of an action list. `contactSupport()` (with `support.email` set), `url()`, and `custom()` always apply, so this only happens when the management screen lists none of them. An active subscription the same user bought through your iOS app reads "Manage this subscription through App Store." One that's still active through any other store the SDK can't manage reads "Manage this subscription where you bought it." Anything else reads "There's nothing to manage for this purchase."

A survey attached to a path is shown before the action runs, except when the path leads to web subscription management, whether or not a management page was found. A survey with no options is also skipped.

### Support email

Contact support opens the user's mail app with a message pre-filled with diagnostics: their user ID, app version, OS version, device model, SDK version, active entitlements, and whether they're in the sandbox. If no mail app is available, the Customer Center shows the address so the user can still reach you.

### Prompting users to update

The Customer Center can show a banner asking the user to update your app, which is useful when someone contacts support about a bug you've already fixed. On Android the SDK doesn't look up your published version, so the banner only appears when you set `latestAppVersion` and the installed version is older:

```kotlin
options.customerCenter = CustomerCenterConfiguration.default.copy(
    support = CustomerCenterConfiguration.Support(
        email = "support@mycompany.com",
        latestAppVersion = "2.1.0",
    ),
)
```

Versions are compared on their first three numeric components, so `2.1` and `2.1.0` are equal. **Update** opens your app's Google Play listing, and **Continue** hides the banner. The banner appears on the management screen only. When a restore finds nothing while the banner would show, the result dialog also offers **Update**, on the management or no purchases screen. Set `warnsAboutUpdates` to `false` to turn off both the banner and that button. After the user taps **Continue**, neither appears again until the Customer Center is presented again.

### Appearance

By default the Customer Center tints its rows, buttons, and spinners with your app's `colorPrimary`, taken from the theme of the activity you present it from (or your application's theme), and matches light and dark mode automatically. A theme you apply at runtime with `setTheme` isn't visible to the SDK, so it uses the manifest theme instead. If your theme leaves `colorPrimary` at a Material or AppCompat default, it falls back to a system blue. Set `appearance.accent` to choose the tint yourself.

## Web checkout purchases

Subscriptions purchased through [web checkout](/docs/android/guides/web-checkout) appear in the Customer Center alongside Google Play purchases, labeled with the store they came from: Stripe and Paddle purchases show "Web", and Superwall-store purchases show "Superwall". An entitlement granted outside any store is also labeled "Superwall". For all of them, the manage subscription path opens their web management page in a Custom Tab instead of Google Play. The SDK derives that page automatically for Superwall-hosted checkouts; set `support.webManagementUrl` to point somewhere else. If no management page can be resolved, the row stays visible and tells the customer to use the link in their emailed receipt. An entitlement granted outside any store is the exception: its row only appears when a management page resolves.

When the user is actively subscribed both on the web and through Google Play, the Customer Center shows a banner warning them they may be paying twice, so they can cancel one. Disable this with `warnsAboutDuplicateSubscriptions = false`.

## Reacting to what users do

Pass a [`CustomerCenterDelegate`](/docs/android/sdk-reference/CustomerCenterDelegate) to `presentCustomerCenter()` to gate restores, handle custom paths, and observe survey answers, refund requests, and dismissal. Custom paths do nothing on their own, so route them from `customerCenterDidSelectAction`:

```kotlin
val delegate = object : CustomerCenterDelegate {
    override fun customerCenterDidSelectAction(
        action: CustomerCenterAction,
        pathId: String,
        purchase: CustomerCenterPurchase?,
    ) {
        if (action is CustomerCenterAction.Custom) {
            openBillingSettings(action.identifier)
        }
    }
}

Superwall.instance.presentCustomerCenter(delegate = delegate)
```

Google Play takes refund requests on its own pages, so the SDK can't see how one ends. A refund reported as `SUCCESS` means Google Play's order history opened, not that a refund was granted. If it couldn't be opened, the status is `ERROR` and the purchase card shows an error message.

## Events

The Customer Center tracks five [`SuperwallEvent`](/docs/android/sdk-reference/SuperwallEvent) cases. Observe them via the [Superwall delegate](/docs/android/guides/using-superwall-delegate) to forward them to your analytics, and see them in your Superwall dashboard like any other event:

| Event                           | Fires when                                                       | Parameters                                                                            |
| ------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `customerCenter_open`           | The Customer Center is presented.                                | `screen` (`management` or `no_purchases`), `presentation` (always `sheet` on Android) |
| `customerCenter_close`          | The Customer Center is dismissed.                                |                                                                                       |
| `customerCenter_action`         | The user taps a path.                                            | `action`, `path_id`, `product_id`, plus `url` or `custom_identifier` when relevant    |
| `customerCenter_surveyResponse` | The user answers a survey.                                       | `survey_id`, `option_id`, `action`, `path_id`, `product_id`                           |
| `customerCenter_refundRequest`  | A refund request is handed off to Google Play, or fails to open. | `product_id`, `status` (`success` or `error`)                                         |

The `url` parameter has its query string and fragment removed, since either can carry a token.

## Localization

All Customer Center strings ship localized in 41 locales, including regional variants such as `pt-BR`, `fr-CA`, and Traditional Chinese. The language follows your app's locale, including per-app language settings. Any titles you set in the configuration are used verbatim, so localize custom titles yourself.

To reword a built-in string, declare a string resource with the same name in your app. For example, to relabel the restore row:

```xml
<!-- res/values/strings.xml -->
<string name="superwall_customer_center_path_restore">Restore my purchases</string>
```

An app string in `values/` only replaces the SDK's default. The SDK ships its own translations, including `en-GB` and `en-AU`, and those still show for their locales unless you also override the string in each matching `values-<locale>` folder.

## Limitations

* The Customer Center always presents as its own full-screen activity. It can't be embedded in your own layout or navigation.
* A purchase whose product has no display name is titled with the entitlement it unlocks, or left untitled. It is never titled with the product identifier. Today that applies to every web purchase, because the product catalog does not return a name yet.
* Promotional and win-back offers are not yet supported as a path type.
* The Customer Center is configured in code today. Remote configuration from the Superwall dashboard is coming.

## FAQ

## Which purchases show up in the Customer Center?

Everything in Superwall's customer info, the same data behind `Superwall.instance.customerInfo`. That covers Google Play subscriptions and one-time purchases, purchases made through [web checkout](/docs/android/guides/web-checkout), and any active entitlement Superwall knows about, including ones granted outside a store. Names and prices come from Google Play, falling back to your Superwall product catalog for web products Google Play can't resolve.

## Why isn't Contact support showing?

It stays hidden until you set `support.email`. See [Customizing the Customer Center](#customizing-the-customer-center).

## Why isn't Request a refund showing?

It lives on a subscription's own screen, so open the subscription from the management screen first. It needs a Google Play subscription, and it's hidden during a free trial, on a purchase that was already refunded, and for free products. If you set `windowMillis` on the path, it's also hidden once that long has passed since the purchase. See [When each path appears](#when-each-path-appears).

## Why isn't Cancel subscription showing?

It lives on a subscription's own screen, and for a Google Play subscription it only shows while the subscription is active and set to renew. Once a user has cancelled, there's nothing left to cancel. See [When each path appears](#when-each-path-appears).

## Does Request a refund issue the refund?

No. It opens Google Play's order history, where the user submits the request and Google decides it. The SDK reports `success` once Google Play's page opens, not when a refund is granted.

## Why doesn't the update banner appear?

The Android SDK doesn't look up your published version on Google Play. Set `latestAppVersion` to a version newer than the installed one to show it. The banner only appears on the management screen, so a user with no purchases won't see it. See [Prompting users to update](#prompting-users-to-update).

## Why doesn't the Customer Center appear when I call presentCustomerCenter?

Check that you're calling it from the main thread while your app is in the foreground, and that another Customer Center isn't already presented. If its activity hasn't started within five seconds, the SDK logs an error and calls your `onDismiss` handler. See [Presenting the Customer Center](#presenting-the-customer-center).