Integrations
Use webhooks to get real-time notifications about your app's subscription and payment events. Integrate Superwall with other services.
The integrations page is where you can manage your webhooks and other integrations with Superwall:

Webhooks
Superwall sends webhooks to notify your application about important subscription and payment events in real-time. These webhooks are designed to closely match App Store and other revenue provider events, minimizing migration difficulty.
Important Design Principle: Webhook events are structured so that summing proceeds
or price
across all events (without filtering) accurately represents total revenue net of refunds. To calculate gross revenue, filter out events with negative proceeds.
Webhook Payload Structure
Every webhook sent by Superwall contains the following structure:
{
"object": "event",
"type": "renewal",
"projectId": 3827,
"applicationId": 1,
"timestamp": 1754067715103,
"data": {
"id": "42fc6339-dc28-470b-a0fa-0d13c92d8b61:renewal",
"name": "renewal",
"cancelReason": null,
"exchangeRate": 1.0,
"isSmallBusiness": false,
"periodType": "NORMAL",
"countryCode": "US",
"price": 9.99,
"proceeds": 6.99,
"priceInPurchasedCurrency": 9.99,
"taxPercentage": 0,
"commissionPercentage": 0.3,
"takehomePercentage": 0.7,
"offerCode": null,
"isFamilyShare": false,
"expirationAt": 1756659704000,
"transactionId": "700002054157982",
"originalTransactionId": "700002050981465",
"originalAppUserId": "$SuperwallAlias:7152E89E-60A6-4B2E-9C67-D7ED8F5BE372",
"store": "APP_STORE",
"purchasedAt": 1754067704000,
"currencyCode": "USD",
"productId": "com.example.premium.monthly",
"environment": "PRODUCTION",
"isTrialConversion": false,
"newProductId": null,
"bundleId": "com.example.app",
"ts": 1754067710106
}
}
Webhook Payload Fields
Field | Type | Description |
---|---|---|
object | string | Always "event" |
type | string | The event type (e.g., "initial_purchase", "renewal", "cancellation") |
projectId | number | Your Superwall project ID |
applicationId | number | Your Superwall application ID |
timestamp | number | Event timestamp in milliseconds since epoch |
data | object | Event-specific data (see below) |
Event Data Object
The data
field contains detailed information about the subscription or payment event:
Event Data Fields
Field | Type | Description |
---|---|---|
id | string | Unique identifier for this event |
name | string | Event name (see Event Names) |
cancelReason | string or null | Reason for cancellation (see Cancel Reasons) |
exchangeRate | number | Exchange rate used to convert to USD |
isSmallBusiness | boolean | Small business program participant |
periodType | string | Period type: TRIAL , INTRO , or NORMAL |
countryCode | string | ISO country code (e.g., "US") |
price | number | Transaction price in USD (negative for refunds) |
proceeds | number | Net proceeds in USD after taxes and fees |
priceInPurchasedCurrency | number | Price in original currency |
taxPercentage | number or null | Tax percentage applied |
commissionPercentage | number | Store commission percentage |
takehomePercentage | number | Your percentage after commission |
offerCode | string or null | Promotional offer code used |
isFamilyShare | boolean | Family sharing purchase |
expirationAt | number or null | Expiration timestamp (milliseconds) |
transactionId | string | Current transaction ID |
originalTransactionId | string | Original transaction ID (subscription ID) |
originalAppUserId | string or null | Original app user ID (see details) |
store | string | Store: APP_STORE , PLAY_STORE , STRIPE , or PADDLE (see note below) |
purchasedAt | number | Purchase timestamp (milliseconds) |
currencyCode | string | ISO currency code for priceInPurchasedCurrency |
productId | string | Product identifier |
environment | string | PRODUCTION or SANDBOX |
isTrialConversion | boolean | Trial to paid conversion |
newProductId | string or null | New product ID (for product changes) |
bundleId | string | App bundle identifier |
ts | number | Event timestamp (milliseconds) |
expirationReason | string (optional) | Reason for expiration (see Cancel Reasons) |
checkoutContext | object (optional) | Stripe-specific checkout context |
Note on Store field: iOS and Android apps can receive events from any payment provider. For example, an iOS app can receive STRIPE
or PADDLE
events when users purchase through Superwall's App2Web features, which allow web-based checkout flows within mobile apps. The store
field indicates where the payment was processed, not which platform the app runs on.
Event Names
Event Name | Value | Description |
---|---|---|
Initial Purchase | initial_purchase | First-time subscription or purchase |
Renewal | renewal | Subscription renewal |
Cancellation | cancellation | Subscription cancelled |
Uncancellation | uncancellation | Subscription reactivated |
Expiration | expiration | Subscription expired |
Billing Issue | billing_issue | Payment processing failed |
Product Change | product_change | User changed subscription tier |
Subscription Paused | subscription_paused | Subscription temporarily paused |
Non-Renewing Purchase | non_renewing_purchase | One-time purchase |
Test | test | Test event for webhook verification |
Period Types
Period Type | Value | Description |
---|---|---|
Trial | TRIAL | Free trial period |
Intro | INTRO | Introductory offer period (discounted rate) |
Normal | NORMAL | Regular subscription period (full price) |
Stores
Store | Value | Description |
---|---|---|
App Store | APP_STORE | Apple App Store |
Play Store | PLAY_STORE | Google Play Store |
Stripe | STRIPE | Stripe payments |
Paddle | PADDLE | Paddle payments (coming soon) |
Environments
Environment | Value | Description |
---|---|---|
Production | PRODUCTION | Live production transactions |
Sandbox | SANDBOX | Test transactions (not real money) |
Cancel/Expiration Reasons
Reason | Value | Description |
---|---|---|
Billing Error | BILLING_ERROR | Payment method failed |
Customer Support | CUSTOMER_SUPPORT | Cancelled via support |
Unsubscribe | UNSUBSCRIBE | User-initiated cancellation |
Price Increase | PRICE_INCREASE | Cancelled due to price change |
Developer Initiated | DEVELOPER_INITIATED | Cancelled programmatically |
Unknown | UNKNOWN | Reason not specified |
Common Use Cases
Detecting Trial Starts
if (
event.data.periodType === "TRIAL" &&
event.data.name === "initial_purchase"
) {
// New trial started
}
Detecting Trial Conversions
if (
event.data.name === "renewal" &&
(event.data.isTrialConversion ||
event.data.periodType === "TRIAL" ||
event.data.periodType === "INTRO")
) {
// Trial or intro offer converted to paid subscription
}
Detecting Trial Cancellations
if (event.data.periodType === "TRIAL" && event.data.name === "cancellation") {
// Trial cancelled
}
Detecting Trial Uncancellations (Reactivations)
if (event.data.periodType === "TRIAL" && event.data.name === "uncancellation") {
// Trial reactivated after cancellation
}
Detecting Trial Expirations
if (event.data.periodType === "TRIAL" && event.data.name === "expiration") {
// Trial expired
}
Detecting Intro Offer Starts
if (
event.data.periodType === "INTRO" &&
event.data.name === "initial_purchase"
) {
// Intro offer started
}
Detecting Intro Offer Cancellations
if (event.data.periodType === "INTRO" && event.data.name === "cancellation") {
// Intro offer cancelled
}
Detecting Intro Offer Uncancellations
if (event.data.periodType === "INTRO" && event.data.name === "uncancellation") {
// Intro offer reactivated
}
Detecting Intro Offer Expirations
if (event.data.periodType === "INTRO" && event.data.name === "expiration") {
// Intro offer expired
}
Detecting Intro Offer Conversions
if (event.data.periodType === "INTRO" && event.data.name === "renewal") {
// Intro offer converted to regular subscription
}
Detecting Subscription Starts
if (
event.data.periodType === "NORMAL" &&
event.data.name === "initial_purchase"
) {
// New paid subscription started
}
Detecting Renewals
if (
event.data.name === "renewal" &&
event.data.periodType === "NORMAL" &&
!event.data.isTrialConversion
) {
// Regular subscription renewal
}
Detecting Refunds
if (event.data.price < 0) {
// Refund processed
const refundAmount = Math.abs(event.data.price);
}
Detecting Cancellations
if (event.data.name === "cancellation") {
// Subscription cancelled
// Check cancelReason for details
const reason = event.data.cancelReason;
}
Detecting Subscription Expirations
if (event.data.name === "expiration") {
// Subscription expired
// Check expirationReason for details
}
Detecting Billing Issues
if (event.data.name === "billing_issue") {
// Payment failed - subscription at risk
}
Detecting Subscription Pauses
if (event.data.name === "subscription_paused") {
// Subscription has been paused
}
Detecting Product Changes
if (event.data.name === "product_change") {
// User changed subscription plan
const oldProduct = event.data.productId;
const newProduct = event.data.newProductId;
}
Detecting Subscription Reactivations
if (event.data.name === "uncancellation") {
// Previously cancelled subscription was reactivated
}
Detecting Non-Renewing Purchases
if (event.data.name === "non_renewing_purchase") {
// One-time purchase completed
}
Detecting Revenue Events
if (event.data.price !== 0 || event.data.name === "non_renewing_purchase") {
// This event involves revenue (positive or negative)
}
Detecting Test Events
if (event.data.name === "test") {
// Test webhook for endpoint verification
}
Revenue Calculation
Total Net Revenue (Including Refunds)
// Sum all proceeds - automatically accounts for refunds
const netRevenue = events.reduce((sum, event) => sum + event.data.proceeds, 0);
Gross Revenue (Excluding Refunds)
// Only sum positive proceeds
const grossRevenue = events.reduce(
(sum, event) => (event.data.proceeds > 0 ? sum + event.data.proceeds : sum),
0
);
Refund Total
// Sum negative proceeds
const refunds = events.reduce(
(sum, event) =>
event.data.proceeds < 0 ? sum + Math.abs(event.data.proceeds) : sum,
0
);
Revenue by Product
const revenueByProduct = {};
events.forEach((event) => {
const productId = event.data.productId;
if (!revenueByProduct[productId]) {
revenueByProduct[productId] = 0;
}
revenueByProduct[productId] += event.data.proceeds;
});
Webhook Security
All webhooks are signed using HMAC-SHA256. Verify the signature before processing:
- Extract the signature from the
X-Webhook-Signature
header - Compute HMAC-SHA256 of the raw request body using your webhook secret
- Compare the computed signature with the received signature
Testing Webhooks
Test events can be identified by:
event.data.name === "test"
- These events have minimal data and are used for webhook endpoint verification
Best Practices
- Always verify webhook signatures to ensure authenticity
- Handle duplicate events - Use
event.id
for idempotency - Process webhooks asynchronously - Return 200 immediately, then process
- Store raw webhook data for debugging and reconciliation
- Handle all event types - Even if you don't process them immediately
- Monitor webhook failures - Implement retry logic for critical events
- Use timestamps - All timestamps are in milliseconds since epoch
Store-Specific Behaviors
Commission Rates by Store
APP_STORE:
- Standard rate: 30%
- Small Business Program rate: 15% (for eligible developers)
- Clean, predictable commission structure
PLAY_STORE:
- Variable rates from 11.8% to 15%
- Most common rate: 15%
- Rates can vary based on region and other factors
STRIPE:
- Variable rates from 0% to ~7.2%
- Generally lower than mobile app stores
- Depends on Stripe pricing plan and transaction type
Price = 0 Events
Events commonly have price = 0
for non-revenue scenarios:
billing_issue
- Payment failed, no money collectedcancellation
- Subscription cancelled, no chargeexpiration
- Subscription expired, no chargeuncancellation
- Reactivation, no immediate chargeproduct_change
- Plan change notificationsubscription_paused
- Pause event, no charge
Revenue events (initial_purchase, renewal, non_renewing_purchase) typically have non-zero prices unless:
- Family sharing scenario (some cases)
- Special promotional offers
- Test transactions
Cancel/Expiration Reasons by Store
APP_STORE:
CUSTOMER_SUPPORT
- Cancelled via Apple supportUNSUBSCRIBE
- User-initiated cancellationBILLING_ERROR
- Payment failure
PLAY_STORE:
- All APP_STORE reasons plus:
UNKNOWN
- Reason not specified or unavailable
STRIPE:
UNKNOWN
- Stripe typically doesn't provide detailed cancellation reasons
Trial Conversions
Expected behavior: isTrialConversion
should only be true
for renewal
events
Offer Codes Support
Store | Support | Notes |
---|---|---|
APP_STORE | ✅ Supported | Rarely used (1.3% of events), typically for win-back campaigns |
PLAY_STORE | ✅ Supported | Heavily used (72.1% of events), complex promotional system |
STRIPE | ❌ Not supported | Offer codes not available in webhook data |
PADDLE | 🔜 Coming soon | Support planned |
Environment Field
All stores support both PRODUCTION and SANDBOX environments:
- PRODUCTION: Live, real-money transactions
- SANDBOX: Test transactions (TestFlight on iOS, test mode on Stripe, test purchases on Play Store)
The environment field helps you filter out test transactions from production analytics.
Store Event Compatibility Matrix
Not all events are available for all stores. This table shows which events you can expect from each store based on real webhook data:
Event Support by Store
Event Name | APP_STORE | PLAY_STORE | STRIPE | PADDLE |
---|---|---|---|---|
billing_issue | ✅ | ✅ | ✅ | 🔜 |
cancellation | ✅ | ✅ | ✅ | 🔜 |
expiration | ✅ | ✅ | ✅ | 🔜 |
initial_purchase | ✅ | ✅ | ✅ | 🔜 |
non_renewing_purchase | ✅ | ✅ | ❌ | 🔜 |
product_change | ✅ | ✅ | ❌ | 🔜 |
renewal | ✅ | ✅ | ✅ | 🔜 |
subscription_paused | ❌ | ✅ | ❌ | 🔜 |
uncancellation | ✅ | ✅ | ✅ | 🔜 |
✅ = Supported | ❌ = Not supported | 🔜 = Coming soon
Period Type Availability by Store
Different stores support different period types for events:
APP_STORE
- Supports all period types (TRIAL, INTRO, NORMAL) for most events
non_renewing_purchase
only occurs with NORMAL period type
PLAY_STORE
- Supports all period types (TRIAL, INTRO, NORMAL) for most events
renewal
only occurs with NORMAL period typesubscription_paused
only occurs with INTRO and NORMAL period types- Unique: Only store that supports
subscription_paused
events
STRIPE
- Limited period type support compared to mobile app stores
- No INTRO period type support observed
expiration
andrenewal
only occur with NORMAL period type- Does not support
non_renewing_purchase
orproduct_change
events
PADDLE
- Coming soon - full support planned!
Store-Specific Considerations
Universal Events (available across APP_STORE, PLAY_STORE, and STRIPE):
billing_issue
cancellation
expiration
initial_purchase
renewal
uncancellation
Store-Specific Events:
subscription_paused
- Only available from PLAY_STOREnon_renewing_purchase
- Not available from STRIPEproduct_change
- Not available from STRIPE
Understanding originalAppUserId
The originalAppUserId
field represents the first app user ID associated with a subscription. This field has specific behavior depending on your integration:
Key Points:
- What it represents: The first user ID we saw associated with this subscription (originalTransactionId)
- Cross-account subscriptions: Since subscriptions are tied to Apple/Google accounts (not app accounts), users can create multiple accounts in your app while using the same subscription
- We only store the first one: If a user creates multiple accounts, we only track the original user ID
When this field is populated:
- iOS/App Store:
- If your user ID has been sent to the stores on-device (via StoreKit)
- If your user IDs are UUIDv4 format
- This field will be consistently present for these cases
- Stripe: Always populated (we create one for you if not provided)
- Play Store: Depends on the integration and user tracking
When this field is null:
- Legacy users: Users on old SDK versions
- Pre-Superwall purchases: Users who purchased before integrating Superwall
- No user ID sent: If user ID was never sent to the store
Understanding originalTransactionId
The originalTransactionId
is Apple's terminology that acts like a subscription ID. For simplicity and consistency with iOS and other revenue tracking platforms, we use this nomenclature and populate it accordingly for all platforms (Play Store, Stripe, Paddle, etc.).
- One per subscription group: Each user subscription gets one
originalTransactionId
- Persists across renewals: The same
originalTransactionId
is used for all renewals in that subscription - Multiple IDs per user: A single user can have multiple
originalTransactionId
if they:- Subscribe to products in different subscription groups
- Let a subscription fully expire and re-subscribe later
- Cross-platform consistency: While originally an Apple concept, we generate and maintain equivalent IDs for all payment providers to ensure consistent subscription tracking
Notes
-
Currency handling:
price
andproceeds
are always in USDpriceInPurchasedCurrency
is in the currency specified bycurrencyCode
exchangeRate
was used to convert from original currency to USD
-
Family Sharing (App Store only):
- When
isFamilyShare
is true withprice > 0
: These are events for the family organizer who pays for the subscription (initial_purchase, renewal, non_renewing_purchase) - When
isFamilyShare
is true withprice = 0
: These are events for family members who use the shared subscription without paying (renewal, uncancellation, billing_issue, etc.)
- When
-
Refunds: Negative values in
price
,proceeds
, orpriceInPurchasedCurrency
indicate refunds -
Transaction IDs:
transactionId
: Unique ID for this specific transactionoriginalTransactionId
: Subscription ID (first transaction in the subscription group)
-
Commission and tax percentages help you understand the revenue breakdown
-
Timestamps:
timestamp
(root level): When the webhook was createdts
(in data): When the actual event occurredpurchasedAt
: When the transaction was originally purchased
Integrations
Currently, we support the following integrations:
- Mixpanel: Track events and user properties in Mixpanel.
- Slack: Send notifications to Slack channels.
- Amplitude: Product analytics for your app.
To set up any of these, click on them and fill in the required fields:
Once you've done that, click the Enable button at the bottom right to save your changes.
Mixpanel Integration - Required Fields
The following fields are required to configure the Mixpanel integration:
Region *
- Description: Data residency region for your Mixpanel project
- Type: Dropdown selection
- Required: Yes
Project Token *
- Description: Your Mixpanel project token
- Type: Text input
- Required: Yes
- Location: Mixpanel → Settings → Project Settings → Project Token
Total Spend Property *
- Description: The name of the user property to track cumulative spend
- Type: Text input
- Required: Yes
Sales Reporting *
- Description: Whether to report Proceeds after store taxes & fees or Revenue
- Type: Dropdown selection
- Required: Yes
- Options:
- Proceeds (after store taxes & fees)
- Revenue
Optional Configuration
Sandbox Project Token
- Description: Optional project token for sandbox events
- Type: Text input
- Required: No
- Note: Leave blank to opt out of sandbox event tracking
Slack Integration - Required Fields
The following fields are required to configure the Slack integration:
Required Configuration
Webhook Url *
- Description: Your Slack webhook URL for sending messages to a channel
- Type: Text input
- Required: Yes
Optional Configuration
Include Sandbox
- Description: Whether to include sandbox events in Slack notifications
- Type: Dropdown selection
- Required: No
Event Type
- Description: Type of events to send: revenue only or all lifecycle (includes trials, cancellations)
- Type: Dropdown selection
- Required: No
- Options:
- Revenue only
- All lifecycle (includes trials, cancellations)
Amplitude Integration - Required Fields
The following fields are required to configure the Amplitude integration:
Required Configuration
Region *
- Description: Data residency region for your Amplitude project
- Type: Dropdown selection
- Required: Yes
Api Key *
- Description: Your Amplitude API key
- Type: Text input
- Required: Yes
Sales Reporting *
- Description: Which revenue value to report in Amplitude
- Type: Dropdown selection
- Required: Yes
Optional Configuration
Sandbox Api Key
- Description: Optional API key for sandbox events
- Type: Text input
- Required: No
- Note: Leave blank to opt out of sandbox event tracking
How is this guide?