# 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

# Target Paywalls by Ad Campaign

Show a different paywall to users from a given ad network, campaign, ad set, ad or keyword, using the acquisition attributes Superwall sets on each install.

Users who install from an ad can see a paywall made for that ad. Superwall stores where each install came from as `acquisition_` user attributes, like `acquisition_network` and `acquisition_campaign_name`. Filter an audience on them to give users from a network, campaign, ad set, ad or keyword their own paywall, such as one that repeats the offer in the ad they tapped.

## Requirements

* An app with install attribution. Stripe (web) apps don't have it, so their audience filters have no **Attribution** section.
* At least one source of attribution:
  * [Superwall MMP](/docs/integrations/mmp), through the [Meta Ads](/docs/integrations/meta-ads) or [Google Ads](/docs/integrations/google-ads) integration. See each page's requirements for SDK versions.
  * The [Apple Search Ads](/docs/integrations/apple-search-ads) integration, Basic or Advanced. Names, like campaign and keyword names, need Advanced.
  * Another MMP, like AppsFlyer, Adjust, Branch or Singular, with your app passing its attribution to Superwall. See [Use another MMP](#use-another-mmp).

## Show a paywall to users from an ad

## Create an audience

Open the campaign whose placement shows the paywall, and add an audience. See [Adding a new audience](/docs/dashboard/dashboard-campaigns/campaigns-audience#adding-a-new-audience).

## Filter on an attribution attribute

Click **Add filter**, open the **Attribution** section of the property picker, and pick an attribute. For example, **Network** **is** `meta`. Click **Save filters**.

## Add the paywall

On the audience's **Paywalls** tab, click **Add paywall** and pick the paywall made for that ad.

## Put the audience above broader ones

Audiences are checked top to bottom, and the first match wins. Drag the new audience above any audience that matches everyone. See [Reordering audiences](/docs/dashboard/dashboard-campaigns/campaigns-audience#reordering-audiences).

Users whose install isn't attributed don't have these attributes, so they skip this audience and fall through to the next one.

## Examples

| To show a paywall to users from | Filter                                                                                        |
| ------------------------------- | --------------------------------------------------------------------------------------------- |
| A network                       | **Network** (`acquisition_network`) **is** `meta`                                             |
| Any paid install                | **Channel** (`acquisition_channel`) **is** `paid`                                             |
| One campaign                    | **Campaign ID** (`acquisition_campaign_id`) **is** your campaign's ID                         |
| A group of campaigns            | **Campaign name** (`acquisition_campaign_name`) **contains** a word they share, like `summer` |
| An ad set or ad group           | **Ad set ID** (`acquisition_ad_set_id`) **is** its ID                                         |
| One ad                          | **Ad ID** (`acquisition_ad_id`) **is** its ID                                                 |
| An Apple Search Ads keyword     | **Keyword name** (`acquisition_keyword_name`) **is** the keyword. Needs Advanced search ads   |
| A Google Ads keyword            | **Keyword** (`acquisition_keyword`) **is** the keyword, for Search campaigns                  |
| A Custom Product Page           | **Custom product page ID** (`acquisition_product_page_id`) **is** the page's ID               |

Combine filters to narrow it down, like **Network** **is** `meta` and **Ad set name** **contains** `trial`. IDs don't change when a campaign is renamed, so prefer them when you target one campaign.

## Attribution attributes

When you add a filter, the property picker lists these in an **Attribution** section above **User**. The section always lists them, even before your app has any attribution data. Until your app sends at least one `acquisition_` attribute, the section explains how to fill them in, with a **Set up Superwall MMP** button when it's available for your app.

<TypeTable
  propColumnWidth="50%"
  type="{
  acquisition_network: {
    description: 'Network. The ad network that drove the install. The picker suggests `meta`, `tiktok`, `google_ads`, `pinterest`, `chatgpt_ads`, `snapchat`, `apple_search_ads` and `custom`.',
    type: 'string',
  },
  acquisition_source: {
    description: 'Source. The source of the install. Superwall MMP and Apple Search Ads set it to the same value as `acquisition_network`.',
    type: 'string',
  },
  acquisition_channel: {
    description: 'Channel. The acquisition channel. Superwall MMP and Apple Search Ads set it to `paid`.',
    type: 'string',
  },
  acquisition_campaign_name: {
    description: 'Campaign name.',
    type: 'string',
  },
  acquisition_campaign_id: {
    description: 'Campaign ID.',
    type: 'string',
  },
  acquisition_ad_set_name: {
    description: 'Ad set name. For Apple Search Ads, the ad group name.',
    type: 'string',
  },
  acquisition_ad_set_id: {
    description: 'Ad set ID. For Apple Search Ads and Google Ads, the ad group ID.',
    type: 'string',
  },
  acquisition_ad_name: {
    description: 'Ad name.',
    type: 'string',
  },
  acquisition_ad_id: {
    description: 'Ad ID.',
    type: 'string',
  },
  acquisition_site_source_name: {
    description: 'Site source. Where the ad ran.',
    type: 'string',
  },
  acquisition_placement: {
    description: 'Placement. The placement that showed the ad.',
    type: 'string',
  },
  acquisition_product_page_id: {
    description: 'Custom product page ID. The App Store Custom Product Page the install came from.',
    type: 'string',
  },
}"
/>

Any other `acquisition_` attribute your app has sent also appears in the section, with a readable label. For example, Apple Search Ads sets `acquisition_keyword_name`, which appears as **Keyword name**.

The filters are saved as user attributes, like `user.acquisition_network`, so audiences that already filter on these attributes under **User** keep working.

## Where the attributes come from

| Source                                                  | What it sets                                                                                                                                                                                                                                                                                                                                    |
| ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Superwall MMP](/docs/integrations/mmp)                 | Network, channel, campaign, ad set, ad, placement and site source from the ad link, plus the Custom Product Page ID. See [Meta Ads user attributes](/docs/integrations/meta-ads#user-attributes) and [Google Ads attributes](/docs/integrations/google-ads#how-attribution-works). Google Ads sets IDs, not names, plus the keyword and device. |
| [Apple Search Ads](/docs/integrations/apple-search-ads) | `apple_search_ads` as the network, and IDs for the campaign, ad group, ad and keyword. Advanced search ads adds the campaign, ad group and keyword names. Apple doesn't name ads.                                                                                                                                                               |
| Another MMP                                             | Whatever your app passes with `setUserAttributes`. See [Use another MMP](#use-another-mmp).                                                                                                                                                                                                                                                     |

### Your own parameters on Superwall ad links

Any other query parameter you add to a Superwall ad link reaches matched installs as `acquisition_` plus the parameter's name. For example, `utm_content=spring` arrives as `acquisition_utm_content` with the value `spring`, ready to filter on without an app update.

* Names are lowercased, and characters other than letters, digits and underscores become underscores. Names are cut at 40 characters, and values at 200.
* A link carries up to 20 of your own parameters.
* Superwall ignores a parameter whose name matches a built-in attribute or an ad network click ID, like `fbclid`, and a value that's still an unfilled placeholder, like `{{campaign.id}}`, or looks like an email address.

## Use another MMP

If AppsFlyer, Adjust, Branch, Singular or another MMP attributes your installs, read the attribution from its SDK callback and pass it to Superwall with [`setUserAttributes`](/docs/sdk/quickstart/setting-user-properties), using the `acquisition_` keys above. The values then appear under **Attribution** and you can target them.

For `acquisition_network`, use a consistent, lowercase value, like your MMP's media source or network name, so your filters match reliably. Set only the attributes your MMP returns a value for.

**AppsFlyer** ([conversion data](https://support.appsflyer.com/hc/en-us/articles/360000726098-Conversion-data-payloads-and-scenarios), from `onConversionDataSuccess`, when `af_status` is `Non-organic`):

| AppsFlyer field | Superwall attribute            |
| --------------- | ------------------------------ |
| `media_source`  | `acquisition_network`          |
| `campaign`      | `acquisition_campaign_name`    |
| `campaign_id`   | `acquisition_campaign_id`      |
| `adset`         | `acquisition_ad_set_name`      |
| `adset_id`      | `acquisition_ad_set_id`        |
| `adgroup`       | `acquisition_ad_name`          |
| `adgroup_id`    | `acquisition_ad_id`            |
| `af_siteid`     | `acquisition_site_source_name` |

Some networks return their ad set and ad in `af_adset`, `af_adset_id`, `af_ad`, and `af_ad_id` instead. Check AppsFlyer's payload for each network you run.

**Adjust** ([attribution object](https://dev.adjust.com/en/sdk/ios/features/attribution/), from the attribution callback):

| Adjust field | Superwall attribute         |
| ------------ | --------------------------- |
| `network`    | `acquisition_network`       |
| `campaign`   | `acquisition_campaign_name` |
| `adgroup`    | `acquisition_ad_set_name`   |
| `creative`   | `acquisition_ad_name`       |

**Branch** ([link data](https://help.branch.io/marketer-hub/docs/deep-link-reference), from the session callback, when `+clicked_branch_link` and `+is_first_session` are `true`):

| Branch field   | Superwall attribute         |
| -------------- | --------------------------- |
| `~channel`     | `acquisition_network`       |
| `~campaign`    | `acquisition_campaign_name` |
| `~campaign_id` | `acquisition_campaign_id`   |
| `~ad_set_name` | `acquisition_ad_set_name`   |
| `~ad_set_id`   | `acquisition_ad_set_id`     |
| `~ad_name`     | `acquisition_ad_name`       |
| `~ad_id`       | `acquisition_ad_id`         |
| `~placement`   | `acquisition_placement`     |

> **Note:** Branch doesn't return [Meta Ads attribution](https://help.branch.io/marketer-hub/docs/enable-meta-ads-new) in the session callback, so these fields are empty for installs from Meta Ads.

For example, with Adjust. This skips organic installs and lowercases the network:

## Tab

```swift iOS
func adjustAttributionChanged(_ attribution: ADJAttribution?) {
  guard let attribution,
        let network = attribution.network,
        network != "Organic" else { return }
  let attributes: [String: Any?] = [
    "acquisition_network": network.lowercased(),
    "acquisition_campaign_name": attribution.campaign,
    "acquisition_ad_set_name": attribution.adgroup,
    "acquisition_ad_name": attribution.creative,
  ]
  // A nil value removes the attribute, so drop nils first.
  let nonNil: [String: Any] = attributes.compactMapValues { $0 }
  Superwall.shared.setUserAttributes(nonNil)
}
```

## Tab

```kotlin Android
adjustConfig.setOnAttributionChangedListener { attribution ->
  val network = attribution.network
  if (network == null || network == "Organic") return@setOnAttributionChangedListener
  val attributes = mapOf(
    "acquisition_network" to network.lowercase(),
    "acquisition_campaign_name" to attribution.campaign,
    "acquisition_ad_set_name" to attribution.adgroup,
    "acquisition_ad_name" to attribution.creative,
  ).filterValues { it != null }
  Superwall.instance.setUserAttributes(attributes)
}
```

> **Warning:** Use one attribution source for these attributes. Superwall MMP and Apple Search Ads write the same `acquisition_` keys and overwrite any matching values you set. Keys they don't write keep your values, so mixing sources leaves a user with attributes from more than one source.

## When the attributes arrive

Attribution isn't always there the moment your app opens, so a paywall on the first launch can be shown before it arrives.

* **Superwall MMP** matches the install once, on first launch. With iOS SDK (SuperwallKit) 4.18.0 or later, or Android SDK 2.9.0 or later, a paywall on that launch waits up to 2 seconds for the match when its placement's audiences use an `acquisition_` attribute. If the match takes longer, the audiences are checked without it.
* **Apple Search Ads** attribution comes from Apple's servers after install, and a paywall doesn't wait for it. If it isn't ready, the SDK tries again when the app returns to the foreground.
* **Another MMP**'s attributes are set when your code calls `setUserAttributes` from the MMP's callback. A paywall doesn't wait for them.

Audiences are checked each time a placement fires, so once the attributes are set, the next placement can match your attribution audience. If the first paywall matters most, such as one on `app_install` or `app_launch`, expect some ad users to see your default paywall there.

## Things to know

* **Attribution only applies to new installs.** Superwall MMP matches an install once, within 7 days of the ad click. Users who installed before your app had Superwall MMP set up never get attributes from it.
* **The attributes survive a reset.** After `reset()` or identifying a different user, the SDK sets the Superwall MMP and Apple Search Ads attributes again for the new user. Attributes you set yourself with `setUserAttributes` are cleared, like any user attribute.
* **Opting out of tracking stops the match.** With `eventTrackingBehavior` set to `.none` (`NONE` on Android), Superwall MMP doesn't match the install, so these attributes stay empty and attribution audiences don't match. `.superwallOnly` keeps the match.
* **Values match as sent.** Filters compare the stored value. Values from another MMP arrive as your code sends them, so lowercase the network as in the example above.

## Related

* [Superwall as your MMP](/docs/integrations/mmp)
* [Meta Ads](/docs/integrations/meta-ads)
* [Apple Search Ads](/docs/integrations/apple-search-ads)
* [Audiences](/docs/dashboard/dashboard-campaigns/campaigns-audience)
* [Setting User Attributes](/docs/sdk/quickstart/setting-user-properties)