---
id: "tools/paywalls/paywall-events"
title: "Paywall events"
description: "RevenueCat Paywalls generate events as Customers view and interact with them. You can receive these events in two ways:"
permalink: "/docs/tools/paywalls/paywall-events"
slug: "paywall-events"
version: "current"
original_source: "docs/tools/paywalls/paywall-events.mdx"
---

> **AI agents:** This is the Markdown version of a RevenueCat documentation page. For the complete documentation index, see [llms.txt](https://www.revenuecat.com/docs/llms.txt).

RevenueCat [Paywalls](https://www.revenuecat.com/docs/tools/paywalls/displaying-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](#sending-events-to-integrations) you've enabled. For callback names on each platform, see [Callbacks by platform](#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](https://www.revenuecat.com/docs/tools/paywalls/creating-paywalls#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_impression` each 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](https://www.revenuecat.com/docs/tools/paywalls/creating-paywalls#multi-step-paywalls), `paywall_impression` fires 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](https://www.revenuecat.com/docs/tools/paywalls/creating-paywalls#multi-step-paywalls), the close callback is a flow close: it fires whenever the flow closes, from any step. `paywall_close` fires only if the flow closes while a paywall step is showing. Moving between steps fires no close event. So your close events can outnumber `paywall_close`.
- **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](https://www.revenuecat.com/docs/tools/paywalls/displaying-paywalls#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](#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](https://www.revenuecat.com/docs/integrations/integrations). For the full list of callbacks on each platform, see [Displaying Paywalls](https://www.revenuecat.com/docs/tools/paywalls/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](https://www.revenuecat.com/docs/integrations/third-party-integrations/supporting-files/paywall-component-interaction-events). Properties that don't apply to an interaction are omitted.

**SwiftUI**

```swift
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
        }
    }
```

**Swift**

```swift
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
        }
    }

}
```

**Kotlin**

```kotlin
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)
}
```

**Kotlin MP**

```kotlin
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)
```

**Flutter**

```dart
import 'package:purchases_ui_flutter/purchases_ui_flutter.dart';

// Embedded paywall
@override
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
      }
    },
  );
}
```

**React Native**

```jsx
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
        }
      },
    },
  });
}
```

**Capacitor**

```jsx
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
        }
      },
    },
  });
}
```

**Unity**

```csharp
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));
    }
}
```

**Web (JS/TS)**

```tsx
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](https://www.revenuecat.com/docs/integrations/third-party-integrations/amplitude) | Analyze paywall engagement alongside subscription lifecycle events in your Amplitude project      |
| [Mixpanel](https://www.revenuecat.com/docs/integrations/third-party-integrations/mixpanel)   | Track paywall views and interactions in your Mixpanel project for cohort and funnel analysis      |
| [PostHog](https://www.revenuecat.com/docs/integrations/third-party-integrations/posthog)     | Combine paywall events with your product analytics in PostHog                                     |
| [Segment](https://www.revenuecat.com/docs/integrations/third-party-integrations/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](https://www.revenuecat.com/docs/tools/paywalls/creating-paywalls#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](https://www.revenuecat.com/docs/tools/paywalls/creating-paywalls#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](https://www.revenuecat.com/docs/integrations/third-party-integrations/supporting-files/paywall-component-interaction-events) 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:

1. Navigate to **Integrations** in your project and open the relevant integration
2. Click the **Paywall Events** tab
3. Check **Send Paywall events**
4. Optionally customize the event names
5. Save your changes

## Next steps

- [Display a Paywall](https://www.revenuecat.com/docs/tools/paywalls/displaying-paywalls) in your app so it starts generating events
- [Create a Paywall](https://www.revenuecat.com/docs/tools/paywalls/creating-paywalls) on the Dashboard if you don't have one yet
- [Analyze funnel integrations](https://www.revenuecat.com/docs/tools/funnels/integrations) if you also run RevenueCat Funnels
