> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sequenzy.com/llms.txt
> Use this file to discover all available pages before exploring further.

# RevenueCat Integration

> Turn RevenueCat in-app purchase webhooks into subscriber events, tags, and revenue attributes

Connect RevenueCat to trigger automations from in-app purchases on the App Store, Google Play, Stripe, and every other store RevenueCat supports. Trials, renewals, cancellations, billing issues, and refunds become Sequenzy events, and each contact gets `mrr`, `ltv`, and billing attributes, without running your own webhook relay.

## What is RevenueCat?

[RevenueCat](https://www.revenuecat.com) manages in-app subscriptions and purchases for iOS, Android, and web apps. It validates receipts with each store and sends one consistent webhook for every subscription change.

## How contacts are matched

RevenueCat identifies people by **app user ID**, not email. Sequenzy matches each event to a contact in this order:

1. A contact whose `externalId` is the event's app user ID, original app user ID, a previous alias, or the user it was transferred to
2. The `$email` subscriber attribute on the event
3. The `$email` attribute read from the RevenueCat API (the SDK syncs attributes in the background, so an early purchase event can arrive without it)
4. An app user ID that is itself an email address

If your backend already creates contacts in Sequenzy, set their `externalId` to the same user ID you pass to `Purchases.logIn()`. Otherwise, set the email in your app:

```swift theme={null}
Purchases.shared.attribution.setEmail(user.email)
```

```kotlin theme={null}
Purchases.sharedInstance.setEmail(user.email)
```

Events for app users that match nothing, including anonymous `$RCAnonymousID` users without an email, are skipped and listed as **skipped** in the integration's **Activity**, so you can see which users need an email or `externalId`.

## Connecting RevenueCat

### Step 1: Create a secret API key

1. In RevenueCat, open **Project settings → API keys**
2. Create a **secret API key** for **API v2**
3. Give it read access to **Project configuration → Projects** and **Customer information → Customers**
4. Copy the key (`sk_...`)

Sequenzy uses the key to confirm the project and to look up a customer's `$email` when a webhook leaves it out. It never writes to RevenueCat.

### Step 2: Enter the key in Sequenzy

1. Go to **Settings → Integrations** in Sequenzy
2. Click **Connect** next to RevenueCat
3. Paste the secret API key. Add the **Project ID** (`proj...`) only if the key can access more than one project
4. Click **Continue**

### Step 3: Add the webhook in RevenueCat

1. In RevenueCat, go to **Integrations → Webhooks** and add a new webhook
2. Set the **Webhook URL** to the URL shown in the Sequenzy dialog:
   ```
   https://api.sequenzy.com/api/webhooks/revenuecat/{your-sequenzy-company-id}
   ```
3. Copy the **Authorization header value** from the Sequenzy dialog and paste it into RevenueCat's authorization header field
4. Set the environment to **Production** so test purchases stay out of your revenue numbers
5. Send all event types, then save

<Note>
  If you turned on **webhook signing** in RevenueCat instead, replace the
  generated value in Sequenzy with RevenueCat's signing secret. Sequenzy accepts
  a delivery when either the authorization header or the signature matches.
</Note>

### Step 4: Connect and test

1. Click **Connect RevenueCat** in Sequenzy
2. In RevenueCat, click **Send test event** on the webhook
3. Open **Activity** next to RevenueCat in Sequenzy. The test appears as processed with "The connection works"

## Tracked events

| Event | Triggered when |
| - | - |
| `saas.trial_started` | `INITIAL_PURCHASE` with a free trial |
| `saas.trial_will_end` | Sequenzy's own reminder, 3 days before a live trial ends, or right away for trials of 3 days or less |
| `saas.trial_ended` | The trial converts (`RENEWAL` with `is_trial_conversion`) |
| `saas.trial_cancelled` | Auto-renew is turned off during a trial |
| `saas.purchase` | An `INITIAL_PURCHASE` (not a trial), `RENEWAL`, or `NON_RENEWING_PURCHASE` |
| `saas.purchase.monthly` | The purchase was a monthly subscription |
| `saas.purchase.yearly` | The purchase was an annual subscription |
| `saas.payment_failed` | `BILLING_ISSUE`: the store could not charge a renewal and may retry |
| `saas.cancelled` | Auto-renew is turned off on a paid subscription; access lasts until expiration |
| `saas.churn` | `EXPIRATION`: the subscription or trial ended and access is lost |
| `saas.refund` | The store refunded the latest period |

`UNCANCELLATION` clears a pending cancellation without an event. When a charge fails, RevenueCat also sends a `CANCELLATION` with `cancel_reason` `BILLING_ERROR`; it is not treated as `saas.cancelled`, because the subscriber didn't cancel and the store may still recover the payment. Promotional entitlements you grant in RevenueCat (store `PROMOTIONAL`) emit no events. An `EXPIRATION` caused by a Google Play pause removes `mrr` but is not `saas.churn`. `PRODUCT_CHANGE`, `TRANSFER`, `SUBSCRIPTION_PAUSED`, `SUBSCRIPTION_EXTENDED`, and other types are acknowledged without events, so there is no `saas.upgrade` or `saas.downgrade` for RevenueCat.

Every event includes the RevenueCat context in its properties, for filters and merge tags: `productId`, `store`, `environment`, `periodType`, `appUserId`, `entitlementIds`, `countryCode`, `cancelReason` (on cancellations), `expirationReason` (on expirations), `isTrialConversion` and `renewalNumber` (on renewals), plus `amount` and `currency` on purchases.

## Synced attributes

| Attribute | Type | Description |
| - | - | - |
| `mrr` | Number | Monthly recurring revenue in US dollars, from the latest paid period with a known interval |
| `ltv` | Number | Lifetime value in US dollars: purchases minus refunds |
| `revenuecatCustomerId` | String | The RevenueCat app user ID |
| `revenuecatStore` | String | Store of the latest purchase, such as `APP_STORE`, `PLAY_STORE`, or `STRIPE` |
| `billingPlanId` | String | The store product ID of the subscription |
| `billingInterval` | String | `week`, `month`, `year`, or `one_time`, inferred from the purchase |
| `billingPeriodEnd` | String | When the current period ends |
| `trial_ends_at` | String | When a free trial ends |

Contacts are also tagged `trial`, `customer`, `cancelled`, or `churned` as their subscription changes.

### How revenue is calculated

* Amounts use the price in the customer's currency, converted to US dollars. When the currency can't be converted, RevenueCat's own US dollar price is used
* Prices include the store's commission and any tax RevenueCat reports. They are not your take-home revenue
* MRR comes from the latest paid period whose interval is known. An introductory price counts toward MRR only when the product ID names the interval, until the first full-price renewal
* MRR is removed at `EXPIRATION`, not at cancellation, because access continues until the period ends
* Refunds lower `ltv`. RevenueCat only reports refunds of the latest period

## Things to know

* **No backfill.** RevenueCat has no bulk export Sequenzy can use, so existing subscribers update as their next event arrives (usually their next renewal). Sync Revenue is not available for RevenueCat
* **Intervals are inferred** from the length of the purchased period, or from words like `monthly`, `annual`, or `1y` in the product ID when the period doesn't tell (trials, introductory prices, unusual lengths). A purchase with no inferable interval counts toward `ltv` but not `mrr`
* **One project per workspace.** Connecting a different RevenueCat project replaces the previous connection
* **Contacts don't move.** If a subscription later resolves to a different contact (after a `TRANSFER`, a changed `$email`, or a contact created afterwards with that `externalId`), the earlier contact keeps its `mrr` for that subscription. Set `$email` or `externalId` before the first purchase to avoid this
* **Late deliveries** never overwrite newer subscription state. A late purchase still counts toward `ltv`
* **Retries are safe.** RevenueCat retries failed deliveries with the same event ID, and Sequenzy records each event once

## Troubleshooting

### Events are skipped with "No contact matched this RevenueCat app user"

The app user has no `$email` attribute and no contact has their app user ID as `externalId`. If the message says RevenueCat rejected the saved API key instead, reconnect with a key that has **Customer information** read access. Call `setEmail` after login, or set the contact's `externalId` to the RevenueCat app user ID. Anonymous users who never log in can't be matched.

### Deliveries fail with an authorization error

The authorization header in RevenueCat doesn't match the value saved in Sequenzy. Copy the value again from a fresh connect, or reconnect RevenueCat with the value you set in RevenueCat. Signed deliveries are also rejected when they are more than 5 minutes old, so check your server clock if you replay captured requests.

### Connecting fails with a permissions error

The API key is missing **Projects** or **Customers** read access, or it is an API v1 key or a public SDK key (`appl_...`, `goog_...`). Create a new secret key for API v2 with both read permissions.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.