How to Build a Remote Paywall in Superwall and Present It in an iOS App

Learn how to design a paywall in the Superwall dashboard, connect your App Store products, and present it in your iOS app using placements and campaign triggers.

How Superwall Presents Paywalls

To build effectively, it helps to understand Superwall's mental model. Instead of writing UI code that decides when and what paywall to show, you emit named events called placements from your app, and a campaign in the Superwall dashboard decides what happens when a placement fires. A campaign contains audience rules and, when those rules match, presents a paywall you designed in the editor. This inversion is the whole point: the app declares intent, for example the user tried to access a premium feature, and Superwall decides the response remotely. Because that decision lives in the dashboard, you can change which paywall appears, target it to segments, or turn presentation off entirely without shipping code. The building blocks are therefore three: the paywall itself, designed in the editor and linked to your real products; the campaign, which maps placements to paywalls with audience rules; and the register call in your app, which fires a placement and provides a feature block that runs once the user is entitled. Everything in this guide assembles those three pieces into a working remote paywall. Keeping that separation clear in your head prevents the most common design mistake, which is trying to encode presentation logic in the app when the whole benefit of the tool is moving that logic to the dashboard.

Connecting Your App Store Products

A paywall is only useful if it shows real, purchasable products, so before designing anything, connect your in-app purchase products to Superwall. Your subscriptions and any non-consumable unlocks must already exist in App Store Connect with their product identifiers, and they must be in a state where StoreKit can load them. In the Superwall dashboard you register these product identifiers so the editor can reference them. When you place a product on a paywall, Superwall pulls the localized price, currency, and trial or introductory offer details from StoreKit at runtime rather than from static text, which means the displayed price is always accurate for the user's storefront. Make sure the product identifiers in Superwall exactly match those in App Store Connect, including case, because a mismatch is the most common reason products render blank on a paywall. If you also use a StoreKit configuration file for local testing in Xcode, ensure its product identifiers match too. Getting this mapping right first saves you from debugging an empty paywall later, and it lets the editor show live pricing while you design. Confirm as well that your Paid Applications agreement is active, because even perfectly matched identifiers will not load if the agreement is pending or expired, and newly created products can take time to propagate through Apple's systems before StoreKit returns them.

Designing the Paywall in the Editor

With products connected, open the paywall editor in the Superwall dashboard to design the screen. The editor provides templated components for headlines, feature lists, product selection controls, call-to-action buttons, and legal links, and it supports dynamic variables that render live product data such as price and trial length. Start from a template if one fits, then adjust copy, imagery, and the arrangement of plan options. Bind your product buttons to the product identifiers you registered so that tapping a plan initiates the correct purchase. Pay attention to required elements Apple expects on a subscription screen, including a clear description of what the subscription provides, the price and billing period, and links to your terms of service and privacy policy, because App Review will check for these and reject paywalls that omit them. Use the preview to see how the paywall renders with real pricing. Because the paywall is defined here rather than in your app, you can later duplicate it to create experiment variants or seasonal versions without touching code. Keep the design honest and legible; a clean paywall that states the offer plainly tends to convert better and passes review more smoothly than a cluttered one. Auto-renewable subscriptions in particular have strict disclosure requirements, so make sure renewal terms and the fact that a subscription auto-renews until cancelled are stated clearly.

Creating a Campaign and Placement

A paywall does nothing until a campaign routes traffic to it. In the dashboard, create a campaign and define one or more placements, which are simply the named events your app will emit, for example campaign_trigger or feature_unlock. For each placement you configure audience rules that decide who sees a paywall and which one; rules can match on user attributes you set from the app, on device or locale properties, or on subscription status so that already-subscribed users are not shown a paywall. Attach the paywall you designed to the matching rule. You can also add filters so a placement only presents under certain conditions, which is useful for limiting how often users see a paywall. The name you choose for the placement is the contract between your app and Superwall: the register call in code must use exactly this name, character for character. Keep a small, documented set of placement names that map to real moments in your app, such as onboarding completion or a premium feature tap, so the campaign stays understandable as it grows and so engineers know which string to fire from where. Centralizing those names as Swift constants rather than scattering string literals across the codebase prevents a whole class of bug where a placement is renamed in one place but not another and a paywall silently stops appearing.

Firing the Placement From Your App

Now connect the app. Wherever the user reaches a gated moment, call Superwall.shared.register(placement: "campaign_trigger") and pass a feature closure. The pattern is that Superwall evaluates the campaign for that placement; if the audience rules say to present a paywall and the user is not entitled, it shows the paywall, and your feature closure runs only after the user gains access, whether by purchasing or because they were already subscribed. If no paywall is warranted, for example the user already has an active subscription, the feature closure runs immediately. This means you write the gating once and let the dashboard decide the policy. For example, on a premium feature button you call register with your placement name and put the code that opens the premium feature inside the feature block. Avoid scattering purchase logic through your views; instead treat register as the single gateway for a given feature. Because presentation is remote, the same code path can show different paywalls over time without any code change. Make sure configure has already run before any register call, and use a placement name that exactly matches the campaign, or nothing will present. This register-with-a-feature-closure pattern is the idiomatic way to use Superwall, and leaning on it rather than manually presenting paywall view controllers is what keeps the integration small and maintainable.

Handling Outcomes and Delegates

For most features the feature closure is all you need, but Superwall also exposes a delegate and handler options for finer control. You can implement SuperwallDelegate to observe lifecycle events such as a paywall being presented, dismissed, or a purchase completing, which is useful for logging, analytics, and coordinating app state. The register call can also accept a handler that reports when the paywall is presented, dismissed, or skipped, letting you react without waiting for the feature block. Use these hooks to keep your app's state consistent, for example refreshing the UI after a purchase or recording an analytics event when a paywall is shown. If you supplied a purchase controller during configuration, remember that the actual transaction flows through your controller and, if you use it, through your entitlement provider, while these delegate events tell you about presentation and results. Keep delegate implementations lightweight and side-effect-aware, because they can fire frequently and a heavy handler can make the paywall feel sluggish. The combination of a feature closure for gating and a delegate for observation covers virtually every presentation scenario without you having to manage paywall view controllers yourself, which is one of the practical conveniences of the SDK. Forwarding these events into your existing analytics pipeline also gives you an independent record of paywall impressions that you can reconcile against Superwall's own dashboard numbers.

Testing the End-to-End Flow

Before relying on a remote paywall in production, test the whole path. Run the app on a real device signed in with a sandbox Apple ID so StoreKit can process test purchases, and trigger the placement you wired up. Confirm the correct paywall appears, that product prices render from real StoreKit data rather than showing blank, and that completing a sandbox purchase runs your feature closure and unlocks the feature. Test the already-subscribed case too, ideally by using a sandbox account with an active subscription, and confirm the paywall is skipped and the feature runs immediately. Exercise the dismissal path to be sure the feature stays gated when the user cancels. Also verify that changing the paywall in the dashboard, such as editing a headline, is reflected in the app without a rebuild, which demonstrates the remote capability working. If products render empty, revisit the product identifier mapping and your App Store Connect agreements. Once purchases, entitlement gating, skips, and remote edits all behave correctly, your remote paywall is production-ready, and future iteration becomes a dashboard task rather than an engineering release. It is worth building a short QA checklist that walks each important placement through present, purchase, skip, and dismiss, so that regressions from a future SDK upgrade or a careless campaign edit surface in testing rather than in your revenue numbers.

Frequently Asked Questions

What is a placement in Superwall?

A placement is a named event your app emits, such as feature_unlock, that a campaign listens for. When the placement fires, the campaign's audience rules decide whether to present a paywall and which one. The placement name in your register call must exactly match the name configured in the campaign.

How do I present a paywall in code?

Call Superwall.shared.register(placement: "your_placement") and pass a feature closure. Superwall shows a paywall if the campaign rules require it and the user is not entitled; your closure runs once the user has access. If no paywall is needed, the closure runs immediately.

Why is my paywall showing blank prices?

Almost always a product identifier mismatch. The product IDs registered in Superwall must exactly match those in App Store Connect, and the products must be loadable by StoreKit. Also confirm your Paid Applications agreement is active and, for local testing, that any StoreKit configuration file uses matching IDs.

Can I change the paywall without releasing an app update?

Yes. The paywall design, copy, and which paywall a placement presents all live in the dashboard, so you can edit them and see changes in the app without a new build, as long as the products referenced already exist in App Store Connect.

How do I avoid showing a paywall to existing subscribers?

Configure your campaign audience rules to check subscription status so entitled users are excluded, and make sure Superwall knows the user's subscription status, either through its own purchasing or by reporting it from your purchase controller. When the user is entitled, the feature closure runs without a paywall.

How can I observe paywall events in my app?

Implement SuperwallDelegate to receive lifecycle events like presented, dismissed, and purchase completed, or use the register call's handler to react to presented, dismissed, and skipped states. Use these for analytics and to keep app state consistent, keeping the implementations lightweight.