> ## 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.

# Superwall Integration

> Turn Superwall subscription webhooks into subscriber events, tags, and revenue attributes

Connect Superwall to trigger automations from purchases made through your Superwall paywalls on the App Store, Google Play, and Stripe web checkout. 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 Superwall?

[Superwall](https://superwall.com) runs remotely configured paywalls for iOS, Android, and web apps. It processes each store's server notifications and sends one consistent webhook for every subscription change.

## How contacts are matched

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

1. A contact whose `externalId` is the user ID you pass to Superwall's `identify()`, or the subscription's original app user ID
2. The `email` user attribute on the event
3. A 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 `identify()`. Otherwise, set the email as a user attribute in your app:

```swift theme={null}
Superwall.shared.identify(userId: user.id)
Superwall.shared.setUserAttributes(["email": user.email])
```

```kotlin theme={null}
Superwall.instance.identify(userId = user.id)
Superwall.instance.setUserAttributes(mapOf("email" to user.email))
```

Superwall attaches user attributes to every webhook, so the email reaches Sequenzy with the next event. Events for users that match nothing, including anonymous `$SuperwallAlias:` 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`.

Name the user with `firstName` and `lastName` attributes, or a single `name` attribute, to fill the contact's name.

## Connecting Superwall

### Step 1: Add the webhook in Superwall

1. In Sequenzy, go to **Settings → Integrations** and click **Connect** next to Superwall
2. Copy the **Webhook URL** from the dialog:
   ```
   https://api.sequenzy.com/api/webhooks/superwall/{your-sequenzy-company-id}
   ```
3. In Superwall, open **Settings → Integrations → Webhooks** and add a webhook with that URL

### Step 2: Paste the signing secret

1. In Superwall, open the webhook you just added and copy its **signing secret**. It starts with `whsec_`
2. Paste it into the **Signing secret** field in the Sequenzy dialog
3. Click **Connect Superwall**

Superwall has no API key to check, so the first signed delivery confirms the connection.

### Step 3: Confirm it works

Make a sandbox purchase in your app, then open **Activity** next to Superwall in Sequenzy. The event appears as skipped with "Sandbox event received. The connection works". Sandbox purchases never change contacts, tags, or revenue.

<Note>
  StoreKit configuration files don't produce server notifications, so purchases
  made with one never reach Superwall's webhook. Test with a sandbox account
  instead.
</Note>

## 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 `isTrialConversion`) |
| `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 a purchase: any event with a negative price or proceeds |

`uncancellation` clears a pending cancellation without an event. When a charge fails, a `cancellation` with `cancelReason` `BILLING_ERROR` is not treated as `saas.cancelled`, because the subscriber didn't cancel and the store may still recover the payment. An `expiration` caused by a Google Play pause removes `mrr` but is not `saas.churn`. `product_change`, `subscription_paused`, and other types are acknowledged without events, so there is no `saas.upgrade` or `saas.downgrade` for Superwall.

Every event includes the Superwall context in its properties, for filters and merge tags: `productId`, `store`, `environment`, `periodType`, `appUserId`, `countryCode`, `providerEventType` (Superwall's event name, such as `renewal`), `cancelReason` (on cancellations), `expirationReason` (on expirations), `isTrialConversion` (on renewals), `isFamilyShare` and `offerCode` when present, 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 |
| `superwallCustomerId` | String | The Superwall user ID |
| `superwallStore` | String | Store of the latest purchase: `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, Superwall's own US dollar price is used
* Prices include the store's commission and tax. They are not your take-home proceeds
* MRR comes from the latest paid period whose interval is known
* MRR is removed at `expiration`, not at cancellation, because access continues until the period ends
* Refunds lower `ltv`

## Things to know

* **No backfill.** Superwall has no export Sequenzy can use, so existing subscribers update as their next event arrives (usually their next renewal). Sync Revenue is not available for Superwall
* **Sandbox is ignored.** Sandbox events confirm the connection in Activity but never update contacts or revenue
* **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`
* **Connect one source per app.** If RevenueCat already sends these purchases to Sequenzy, keep one of the two connected, or each purchase counts twice
* **One connection per workspace.** Reconnecting replaces the signing secret
* **Contacts don't move.** If a subscription later resolves to a different contact (a changed email or a contact created afterwards with that `externalId`), the earlier contact keeps its `mrr` for that subscription. Call `identify()` and set the email before the first purchase to avoid this
* **Late deliveries** never overwrite newer subscription state. A late purchase still counts toward `ltv`
* **Retries are safe.** Superwall retries failed deliveries with the same event ID, and Sequenzy records each event once

## Troubleshooting

### Events are skipped with "No contact matched this Superwall user"

The user has no `email` attribute and no contact has their user ID as `externalId`. Call `identify()` and `setUserAttributes` after login, or set the contact's `externalId` to the user ID you pass to Superwall. Anonymous users who never log in can't be matched.

### Deliveries fail with a signature error

The signing secret saved in Sequenzy doesn't match the webhook in Superwall. Copy the secret from that webhook again and reconnect Superwall. Deliveries are also rejected when their signature is more than 5 minutes old, so check your server clock if you replay captured requests.

### Nothing appears in Activity

Check that the webhook in Superwall points at the exact URL from the Sequenzy dialog, and that you tested with a sandbox or real purchase rather than a StoreKit configuration file.


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