Paywall events
Receive RevenueCat Paywall events in your app or send them to your analytics platforms
RevenueCat Paywalls generate events as Customers view and interact with them. You can receive these events in two ways:
- In your app, through SDK callbacks that fire at key moments, so you can send your own events to any analytics tool
- In an analytics integration, delivered by RevenueCat alongside your subscription lifecycle events
Both work at the same time. Listening to the callbacks doesn't change what RevenueCat sends to your integrations.
Receiving events in your app
The SDK fires a callback at most of these moments, so you can send your own events from them. RevenueCat also sends the matching event to any integrations you've enabled. For callback names on each platform, see Callbacks by platform.
| Moment | SDK callback | Integration event |
|---|---|---|
| Paywall shown | None | paywall_impression |
| Paywall closed | Close | paywall_close |
| Payment confirmation dismissed without purchasing | Cancel | paywall_cancel |
| Exit offer shown | Exit offer (UIKit) | paywall_exit_offer |
| Paywall control changed | Component interaction | paywall_component_interacted |
For multi-step paywalls, RevenueCat sends paywall_impression and paywall_close only for paywall steps, the steps with packages to purchase. The SDK callbacks cover the whole flow, from the first step to the last.
- Paywall shown: RevenueCat sends
paywall_impressioneach time a paywall is visited. No SDK callback fires at this moment.- For a single-screen paywall, send your own event when you present the paywall.
- For multi-step paywalls,
paywall_impressionfires each time a paywall step is visited, including when the Customer returns to one. You can only send your own event when you present the flow, so it also counts Customers who leave before they visit a paywall step.
- Paywall closed: the close callback fires whenever the paywall closes, including after a purchase.
- For multi-step paywalls, the close callback is a flow close: it fires whenever the flow closes, from any step.
paywall_closefires only if the flow closes while a paywall step is showing. Moving between steps fires no close event. So your close events can outnumberpaywall_close.
- For multi-step paywalls, the close callback is a flow close: it fires whenever the flow closes, from any step.
- Payment confirmation dismissed without purchasing: the paywall stays open.
- Exit offer shown: only UIKit on iOS has a callback. The other platforms that support exit offers show them without one. Kotlin Multiplatform and Web don't support exit offers.
- Paywall control changed: the callback passes the same properties as
paywall_component_interacted. See Component interactions.
Unlike onInteraction, the close, cancel, and exit offer callbacks don't pass paywall properties such as paywall_id or offering_id. Add the ones you need from your own code.
Callbacks by platform
| Platform | Close | Cancel | Exit offer | Component interaction |
|---|---|---|---|---|
| iOS (SwiftUI) | onDismiss on presentPaywall or presentPaywallIfNeeded. For an embedded PaywallView, use your own .onDisappear | .onPurchaseCancelled | No callback | .onPaywallInteraction |
| iOS (UIKit) | paywallViewControllerWasDismissed(_:) | paywallViewControllerDidCancelPurchase(_:) | paywallViewController(_:willPresentExitOfferController:) | paywallViewController(_:didTrackInteraction:) |
| Android | dismissRequest on PaywallOptions, or the PaywallActivityLauncher result | PaywallListener.onPurchaseCancelled | No callback | PaywallListener.onInteraction |
| Kotlin Multiplatform | dismissRequest on PaywallOptions | PaywallListener.onPurchaseCancelled | Not applicable. KMP paywalls don't show exit offers | PaywallListener.onInteraction |
| Flutter | onDismiss on PaywallView, or when presentPaywall completes | onPurchaseCancelled | No callback | onInteraction |
| React Native | onDismiss on RevenueCatUI.Paywall, or when presentPaywall resolves | onPurchaseCancelled | No callback | onInteraction |
| Capacitor | The paywallDismissed listener, or when presentPaywall resolves | listener.onPurchaseCancelled | No callback | listener.onInteraction |
| Unity | When PaywallsPresenter.Present completes | PaywallListener.OnPurchaseCancelled | No callback | PaywallListener.OnInteraction |
| Web | When presentPaywall resolves or rejects | listener.onPurchaseCancelled | Not applicable. Web paywalls don't show exit offers | listener.onInteraction |
On Android, onPurchaseCancelled doesn't fire if you pass your own purchase logic (PurchaseLogic) to the paywall. This includes cross-platform apps running on Android. Track the cancellation in your own purchase code instead.
The SDK also fires callbacks for purchases and restores, such as onPurchaseStarted, onPurchaseCompleted, onPurchaseError, and onRestoreCompleted. RevenueCat doesn't send paywall events for these moments. Purchases reach your integrations as subscription lifecycle events. For the full list of callbacks on each platform, see Displaying Paywalls.
Component interactions
When a Customer interacts with a paywall control, such as a tab, package, or purchase button, the SDK calls your onInteraction callback with a paywall_component_interacted event. The event is a set of snake_case properties that matches the component interaction reference. Properties that don't apply to an interaction are omitted.
- SwiftUI
- Swift
- Kotlin
- Kotlin MP
- Flutter
- React Native
- Capacitor
- Unity
- Web (JS/TS)
PaywallView()
.onPaywallInteraction { event in
analytics.track("paywall_interaction", properties: event.rawProperties)
let componentType = event.property(for: PaywallInteractionEvent.Keys.componentType)
if componentType == PaywallInteractionEvent.ComponentTypes.purchaseButton {
// React to a specific interaction
}
}
extension ViewController: PaywallViewControllerDelegate {
func paywallViewController(_ controller: PaywallViewController,
didTrackInteraction event: PaywallInteractionEvent) {
analytics.track("paywall_interaction", properties: event.rawProperties)
let componentType = event.property(for: PaywallInteractionEvent.Keys.componentType)
if componentType == PaywallInteractionEvent.ComponentTypes.purchaseButton {
// React to a specific interaction
}
}
}
import androidx.compose.runtime.Composable
import com.revenuecat.purchases.ui.revenuecatui.Paywall
import com.revenuecat.purchases.ui.revenuecatui.PaywallInteractionEvent
import com.revenuecat.purchases.ui.revenuecatui.PaywallListener
import com.revenuecat.purchases.ui.revenuecatui.PaywallOptions
@Composable
fun PaywallWithInteractionTracking(
onDismiss: () -> Unit
) {
val options = PaywallOptions.Builder(dismissRequest = onDismiss)
.setListener(
object : PaywallListener {
override fun onInteraction(event: PaywallInteractionEvent) {
analytics.track("paywall_interaction", event.rawProperties)
val componentType = event.getProperty(PaywallInteractionEvent.Keys.COMPONENT_TYPE)
if (componentType == PaywallInteractionEvent.ComponentTypes.PURCHASE_BUTTON) {
// React to a specific interaction
}
}
}
)
.build()
Paywall(options)
}
import com.revenuecat.purchases.kmp.ui.revenuecatui.Paywall
import com.revenuecat.purchases.kmp.ui.revenuecatui.PaywallInteractionEvent
import com.revenuecat.purchases.kmp.ui.revenuecatui.PaywallListener
import com.revenuecat.purchases.kmp.ui.revenuecatui.PaywallOptions
val options = remember {
PaywallOptions(dismissRequest = { TODO("Handle dismiss") }) {
listener = object : PaywallListener {
override fun onInteraction(event: PaywallInteractionEvent) {
analytics.track("paywall_interaction", event.rawProperties)
val componentType = event.getProperty(PaywallInteractionEvent.Keys.COMPONENT_TYPE)
if (componentType == PaywallInteractionEvent.ComponentTypes.PURCHASE_BUTTON) {
// React to a specific interaction
}
}
}
}
}
Paywall(options)
import 'package:purchases_ui_flutter/purchases_ui_flutter.dart';
// Embedded paywall
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: PaywallView(
onInteraction: (event) {
analytics.track('paywall_interaction', event);
if (event['component_type'] == 'purchase_button') {
// React to a specific interaction
}
},
),
),
);
}
// Presented paywall
void presentPaywall() async {
await RevenueCatUI.presentPaywall(
onInteraction: (event) {
analytics.track('paywall_interaction', event);
if (event['component_type'] == 'purchase_button') {
// React to a specific interaction
}
},
);
}
import React from 'react';
import { View } from 'react-native';
import RevenueCatUI from 'react-native-purchases-ui';
// Embedded paywall
return (
<View style={{ flex: 1 }}>
<RevenueCatUI.Paywall
onInteraction={(event) => {
analytics.track('paywall_interaction', event);
if (event.component_type === 'purchase_button') {
// React to a specific interaction
}
}}
/>
</View>
);
// Presented paywall
async function presentPaywall() {
await RevenueCatUI.presentPaywall({
callbacks: {
onInteraction: (event) => {
analytics.track('paywall_interaction', event);
if (event.component_type === 'purchase_button') {
// React to a specific interaction
}
},
},
});
}
import { PAYWALL_COMPONENT_TYPES } from '@revenuecat/purchases-capacitor';
import { RevenueCatUI } from '@revenuecat/purchases-capacitor-ui';
async function presentPaywall() {
await RevenueCatUI.presentPaywall({
listener: {
onInteraction: (event) => {
analytics.track('paywall_interaction', event);
if (event.component_type === PAYWALL_COMPONENT_TYPES.PURCHASE_BUTTON) {
// React to a specific interaction
}
},
},
});
}
using System.Threading.Tasks;
using RevenueCatUI;
public class ShowPaywallWithInteractionTracking : MonoBehaviour
{
public async Task ShowPaywall()
{
var listener = new PaywallListener
{
OnInteraction = properties =>
{
analytics.Track("paywall_interaction", properties);
if (properties.TryGetValue("component_type", out var componentType)
&& (string)componentType == "purchase_button")
{
// React to a specific interaction
}
}
};
var result = await PaywallsPresenter.Present(new PaywallOptions(listener: listener));
}
}
import { PAYWALL_COMPONENT_TYPES } from "@revenuecat/purchases-js";
await purchases.presentPaywall({
listener: {
onInteraction: (event) => {
analytics.track("paywall_interaction", event);
if (event.component_type === PAYWALL_COMPONENT_TYPES.PURCHASE_BUTTON) {
// React to a specific interaction
}
},
},
});
Where onInteraction is available
| Platform | Minimum version |
|---|---|
| iOS | 5.89.0 |
| Android | 10.21.0 |
| Kotlin Multiplatform | 3.9.0 |
| Flutter | 10.14.0 |
| React Native | 10.13.0 |
| Capacitor | 13.6.0 |
| Unity | 9.11.0 |
| Web | 1.60.0 |
On iOS, Android, and Kotlin Multiplatform the callback receives a PaywallInteractionEvent. Use rawProperties to forward every property, or read a single typed value with the constants in PaywallInteractionEvent.Keys. On the other platforms the callback receives the properties as a plain map or object.
Sending events to integrations
RevenueCat can deliver paywall events to the following integrations, alongside the standard subscription lifecycle events they already receive.
Supported integrations
| Integration | Use case |
|---|---|
| Amplitude | Analyze paywall engagement alongside subscription lifecycle events in your Amplitude project |
| Mixpanel | Track paywall views and interactions in your Mixpanel project for cohort and funnel analysis |
| PostHog | Combine paywall events with your product analytics in PostHog |
| Segment | Route paywall events through your Segment workspace to your warehouse and downstream destinations |
Paywall event types
All four integrations support the same set of paywall-specific events, in addition to the standard RevenueCat subscription lifecycle events they already track:
| Event | Default name | Description |
|---|---|---|
| Paywall impression | paywall_impression | Fired when a paywall is displayed to a Customer. For multi-step paywalls, it fires each time a paywall step (a step with packages to purchase) is visited, not when the flow starts |
| Paywall close | paywall_close | Fired when a Customer closes a paywall. For multi-step paywalls, it fires only if the flow closes while a paywall step is showing |
| Paywall cancel | paywall_cancel | Fired when a Customer dismisses the payment confirmation on a paywall |
| Paywall exit offer | paywall_exit_offer | Fired when an exit offer is shown on a paywall |
| Paywall component interaction | paywall_component_interacted | Fired when a Customer changes a paywall control (tab, package, purchase button, sheet, etc.). See the component interaction reference for field details |
You can customize the name for each event in the integration settings.
Paywall events are not purchase lifecycle events. They don't include transaction fields such as product_id, price, or transaction_id.
Enabling paywall events
Paywall event delivery is opt-in, and you enable it per integration. Set the integration up first using the integration-specific docs linked above, then:
- Navigate to Integrations in your project and open the relevant integration
- Click the Paywall Events tab
- Check Send Paywall events
- Optionally customize the event names
- Save your changes
Next steps
- Display a Paywall in your app so it starts generating events
- Create a Paywall on the Dashboard if you don't have one yet
- Analyze funnel integrations if you also run RevenueCat Funnels