# Customer.io Source: https://superwall.com/docs/integrations/customer-io The Customer.io integration sends subscription lifecycle events from Superwall to Customer.io's Data Pipelines API. This enables you to trigger targeted messaging campaigns, build user segments based on subscription behavior, and track the complete customer journey from trial to paid subscriber. In the **Communication** section within **Integrations**, you can connect your Customer.io account to Superwall: ![](/images/integrations-customer-io.jpeg) ## Features * **Real-time Event Tracking**: Subscription events are sent immediately to Customer.io as they occur * **Multi-Region Support**: Choose between US and EU data residency to comply with data regulations * **Flexible Revenue Reporting**: Report either gross revenue or net proceeds after store fees * **Sandbox Environment Support**: Separate API key for testing without polluting production data * **Anonymous User Handling**: Configurable behavior for users without an identified app user ID * **Custom Event Names**: Remap default event names to match your existing Customer.io conventions * **Automatic User Identification**: Smart routing between `userId` and `anonymousId` based on user state ## Configuration ### Required Settings | Field | Description | Example | | ----------------- | ---------------------------------------------------- | --------------------------- | | `integration_id` | Must be set to `"customerio"` | `"customerio"` | | `region` | Data residency region for your Customer.io workspace | `"US"` or `"EU"` | | `api_key` | Pipelines API key from your HTTP source | `"abc123def456..."` | | `sales_reporting` | Whether to report gross Revenue or net Proceeds | `"Revenue"` or `"Proceeds"` | ### Optional Settings | Field | Description | Default | | ------------------------- | ------------------------------------------------------ | ----------------------------- | | `sandbox_api_key` | Separate Pipelines API key for sandbox/test events | None (sandbox events skipped) | | `anonymous_user_behavior` | How to handle events from users without an app user ID | `"send"` | | `eventNameMappings` | Custom mapping to rename default event names | None | ### Example Configuration ```json { "integration_id": "customerio", "region": "US", "api_key": "your-pipelines-api-key", "sales_reporting": "Revenue", "sandbox_api_key": "your-sandbox-pipelines-api-key", "anonymous_user_behavior": "send", "eventNameMappings": { "sw_trial_start": "trial_started", "sw_subscription_start": "subscription_started", "sw_renewal": "subscription_renewed" } } ``` ## Getting Your API Key The Customer.io integration uses the **Pipelines API** (part of Customer.io Data Pipelines), not the Track API. To get your API key: 1. Log in to your Customer.io account 2. Navigate to **Data Pipelines** in the left sidebar 3. Go to **Sources** 4. Click **Add Source** and select **HTTP** 5. Name your source (e.g., "Superwall Events") 6. Copy the **API Key** displayed after creation **Important**: The Pipelines API key is different from the Track API credentials (Site ID + API Key). Make sure you're using the correct key from Data Pipelines. ## Event Mapping Superwall subscription events are transformed into Customer.io events based on the event type and subscription period. All events are prefixed with `sw_` by default. ### Trial Events | Superwall Event | Condition | Customer.io Event | | ------------------ | -------------------- | ---------------------- | | `INITIAL_PURCHASE` | `periodType = Trial` | `sw_trial_start` | | `CANCELLATION` | `periodType = Trial` | `sw_trial_cancelled` | | `UNCANCELLATION` | `periodType = Trial` | `sw_trial_uncancelled` | | `EXPIRATION` | `periodType = Trial` | `sw_trial_expired` | | `RENEWAL` | `periodType = Trial` | `sw_trial_converted` | ### Intro Offer Events | Superwall Event | Condition | Customer.io Event | | ------------------ | -------------------- | ---------------------------- | | `INITIAL_PURCHASE` | `periodType = Intro` | `sw_intro_offer_start` | | `CANCELLATION` | `periodType = Intro` | `sw_intro_offer_cancelled` | | `UNCANCELLATION` | `periodType = Intro` | `sw_intro_offer_uncancelled` | | `EXPIRATION` | `periodType = Intro` | `sw_intro_offer_expired` | | `RENEWAL` | `periodType = Intro` | `sw_intro_offer_converted` | ### Subscription Events | Superwall Event | Condition | Customer.io Event | | ------------------ | -------------------------- | ----------------------------- | | `INITIAL_PURCHASE` | `periodType = Normal` | `sw_subscription_start` | | `RENEWAL` | `periodType = Normal` | `sw_renewal` | | `RENEWAL` | `isTrialConversion = true` | `sw_trial_converted` | | `CANCELLATION` | `periodType = Normal` | `sw_subscription_cancelled` | | `UNCANCELLATION` | `periodType = Normal` | `sw_subscription_uncancelled` | | `EXPIRATION` | `periodType = Normal` | `sw_subscription_expired` | ### Other Events | Superwall Event | Customer.io Event | | -------------------------- | -------------------------- | | `PRODUCT_CHANGE` | `sw_product_change` | | `BILLING_ISSUE` | `sw_billing_issue` | | `SUBSCRIPTION_PAUSED` | `sw_subscription_paused` | | `NON_RENEWING_PURCHASE` | `sw_non_renewing_purchase` | | Any event with `price < 0` | `sw_refund` | ## Event Properties Each event sent to Customer.io includes comprehensive properties from the original Superwall event, plus additional formatted fields for revenue tracking. ### Standard Properties All events include the complete Superwall event data: | Property | Description | Example | | ----------------------- | ----------------------------------- | --------------------------- | | `id` | Unique event identifier | `"evt_abc123"` | | `productId` | The subscription product ID | `"com.app.premium.monthly"` | | `store` | App store (APP\_STORE, PLAY\_STORE) | `"APP_STORE"` | | `environment` | Production or Sandbox | `"Production"` | | `countryCode` | User's country code | `"US"` | | `currencyCode` | Transaction currency | `"USD"` | | `originalAppUserId` | Your app's user identifier | `"user_12345"` | | `originalTransactionId` | Store's original transaction ID | `"1000000123456789"` | | `transactionId` | Current transaction ID | `"1000000987654321"` | | `purchasedAt` | Purchase timestamp (ms) | `1705312200000` | | `expirationAt` | Subscription expiration (ms) | `1707990600000` | | `periodType` | Trial, Intro, or Normal | `"Normal"` | | `isTrialConversion` | Whether this converts a trial | `true` | | `isFamilyShare` | Family sharing purchase | `false` | | `bundleId` | App bundle identifier | `"com.example.app"` | ### Revenue Properties When the event has a non-zero price, these additional properties are included: | Property | Description | Example | | ----------------- | ----------------------------------------- | --------------------------- | | `price` | Amount based on `sales_reporting` setting | `9.99` | | `currency` | Currency code | `"USD"` | | `product_id` | Product identifier | `"com.app.premium.monthly"` | | `subscription_id` | Original transaction ID | `"1000000123456789"` | | `offer_code` | Promotional offer code (if present) | `"SUMMER2024"` | ### Revenue vs Proceeds The `sales_reporting` setting controls which amount is sent: * **Revenue**: The full price charged to the customer (e.g., $9.99) * **Proceeds**: The amount after store fees are deducted (e.g., $8.49 after Apple's 15-30% commission) ## User Identification Customer.io uses either `userId` or `anonymousId` to identify users. The integration automatically selects the appropriate identifier based on user state. ### Known Users For users with an `originalAppUserId` set in Superwall: ```json { "userId": "user_12345", "event": "sw_subscription_start", "properties": { ... }, "timestamp": "2024-01-15T10:30:00.000Z" } ``` ### Anonymous Users For users without an `originalAppUserId`, the behavior depends on `anonymous_user_behavior`: **When set to `"send"` (default)**: * Events are sent with an `anonymousId` constructed from the store and transaction ID * Format: `$STORE_NAME:originalTransactionId` ```json { "anonymousId": "$APP_STORE:1000000123456789", "event": "sw_subscription_start", "properties": { ... }, "timestamp": "2024-01-15T10:30:00.000Z" } ``` **When set to `"dontSend"`**: * Events from anonymous users are skipped entirely * Useful if you only want to track identified users ## Sandbox Handling The integration supports separate handling for sandbox (test) events: ### With Sandbox API Key Configured When `sandbox_api_key` is provided: * Production events use the main `api_key` * Sandbox events use the `sandbox_api_key` * Both are sent to Customer.io but can be routed to different destinations ### Without Sandbox API Key When `sandbox_api_key` is not provided: * Production events are sent normally * Sandbox events are **skipped entirely** * This prevents test data from polluting your production Customer.io workspace ## Data Residency Customer.io offers data residency in two regions. The integration automatically routes to the correct endpoint: | Region | API Endpoint | | ------ | ------------------------------------- | | US | `https://cdp.customer.io/v1/track` | | EU | `https://cdp-eu.customer.io/v1/track` | Choose the region that matches your Customer.io workspace configuration. Using the wrong region will result in authentication errors. ## Custom Event Names Use `eventNameMappings` to rename default event names to match your existing Customer.io conventions: ```json { "eventNameMappings": { "sw_trial_start": "Started Free Trial", "sw_subscription_start": "Subscribed", "sw_renewal": "Subscription Renewed", "sw_subscription_cancelled": "Subscription Cancelled", "sw_refund": "Refund Processed" } } ``` Only events you specify in the mapping are renamed. All other events keep their default `sw_` prefixed names. ## Testing the Integration ### 1. Validate Credentials The integration validates credentials by sending a test event to Customer.io. If the API key is invalid or the region is incorrect, you'll receive an authentication error. ### 2. Verify in Customer.io After sending test events: 1. Go to **Data Pipelines** → **Sources** → your HTTP source 2. Click on **Events** to see incoming events 3. Verify event names and properties match expectations ### 3. Test Scenarios Verify these scenarios work correctly: * [ ] Production event with known user (should use `userId`) * [ ] Production event with anonymous user (should use `anonymousId` or skip) * [ ] Sandbox event with sandbox API key (should send to Customer.io) * [ ] Sandbox event without sandbox API key (should be skipped) * [ ] Event with custom name mapping (should use remapped name) * [ ] Revenue event (should include `price`, `currency`, `product_id`) * [ ] Non-revenue event like cancellation (should not include revenue properties) ## Best Practices 1. **Use separate sandbox credentials**: Configure a `sandbox_api_key` to keep test data separate from production, or leave it blank to skip sandbox events entirely. 2. **Choose the right sales reporting**: Use "Revenue" for customer-facing metrics and "Proceeds" for financial reporting that accounts for store fees. 3. **Handle anonymous users thoughtfully**: If your app requires login, use `"dontSend"` to avoid cluttering Customer.io with unidentifiable users. 4. **Keep event names consistent**: If you have existing events in Customer.io, use `eventNameMappings` to maintain naming consistency across your data. 5. **Verify your region**: Ensure your `region` setting matches your Customer.io workspace location to avoid authentication failures. 6. **Test with sandbox first**: Always test your integration configuration with sandbox events before going live with production data. ## Common Use Cases ### Win-Back Campaigns Trigger automated campaigns when users cancel: 1. Listen for `sw_subscription_cancelled` events 2. Create a segment of recently cancelled users 3. Send a series of win-back emails with special offers ### Trial Conversion Optimization Improve trial-to-paid conversion: 1. Track `sw_trial_start` to begin a nurture sequence 2. Send educational content about premium features 3. Trigger a special offer before trial expiration 4. Track `sw_trial_converted` to measure success ### Churn Prevention Identify and engage at-risk subscribers: 1. Monitor `sw_billing_issue` events 2. Send immediate notification to update payment method 3. Follow up with helpful support content 4. Track resolution with subsequent `sw_renewal` events ### Revenue Analytics Build comprehensive revenue reporting: 1. Segment users by subscription status 2. Track lifetime value using revenue properties 3. Analyze conversion rates by cohort 4. Measure impact of promotional offers via `offer_code` ## Troubleshooting ### Events Not Appearing in Customer.io **Possible causes:** * Incorrect API key (make sure you're using Pipelines API key, not Track API) * Wrong region selected (US vs EU mismatch) * Sandbox events without sandbox API key configured (events are skipped) * Anonymous users with `dontSend` behavior (events are skipped) **Solution:** Verify your API key is from Data Pipelines → Sources → HTTP, and check that your region matches your workspace. ### Authentication Errors **Possible causes:** * Using Track API credentials instead of Pipelines API key * Region mismatch between configuration and Customer.io workspace * API key has been revoked or regenerated **Solution:** Generate a new HTTP source in Data Pipelines and use the fresh API key. ### Missing Revenue Properties **Possible causes:** * Event has zero price (cancellations, expirations) * Refund events (price is negative, still included but as negative value) **Solution:** Revenue properties (`price`, `currency`, `product_id`, `subscription_id`) are only added when the price is non-zero. This is expected behavior. ### Wrong Event Names **Possible causes:** * Event name mappings not configured * Typo in mapping configuration **Solution:** Check your `eventNameMappings` configuration. Keys should be the default event names (e.g., `sw_trial_start`), and values should be your desired custom names. ## Rate Limits Customer.io's Pipelines API has generous rate limits suitable for high-volume event ingestion: * **Requests**: 500 requests per second per source * **Payload size**: 32KB per request The integration sends one event per webhook, well within these limits. If you experience rate limiting, contact Customer.io support to increase your limits. ## API Reference ### Endpoint ``` POST https://cdp.customer.io/v1/track (US region) POST https://cdp-eu.customer.io/v1/track (EU region) ``` ### Authentication Basic Authentication with the Pipelines API key as username and empty password: ``` Authorization: Basic base64(api_key:) ``` ### Request Format ```json { "userId": "user_12345", "event": "sw_subscription_start", "timestamp": "2024-01-15T10:30:00.000Z", "properties": { "productId": "com.app.premium.monthly", "price": 9.99, "currency": "USD", "store": "APP_STORE", "environment": "Production", ... } } ``` ### Response Success: `200 OK` with empty body or acknowledgment Errors: * `401 Unauthorized`: Invalid API key or wrong region * `400 Bad Request`: Malformed request body * `429 Too Many Requests`: Rate limit exceeded --- # Discord Source: https://superwall.com/docs/integrations/discord The Discord integration sends real-time subscription notifications to your Discord channels via webhooks. Get instant visibility into subscription activity with beautifully formatted, color-coded embed messages that make it easy to monitor revenue, track trials, and respond to billing issues as they happen. In the **Communication** section within **Integrations**, you can connect your Discord account to Superwall: ![](/images/integrations-discord.jpeg) ## Features * **Rich Embed Messages**: Beautiful, color-coded notifications with emoji indicators for quick visual parsing * **Event Type Filtering**: Choose between revenue-only events or all subscription lifecycle events * **Real-Time Notifications**: Instant alerts when subscription events occur * **Smart Formatting**: Currency formatting, country names, and human-readable descriptions * **Flexible Revenue Reporting**: Report either gross revenue or net proceeds after store fees * **Anonymous User Handling**: Configurable behavior for users without an identified app user ID * **Custom Event Names**: Override default event titles to match your team's terminology * **Sandbox Indicators**: Clear visual badge when events come from test environments ## Configuration ### Required Settings | Field | Description | Example | | ----------------- | ----------------------------------------------- | ----------------------------------------------- | | `integration_id` | Must be set to `"discord"` | `"discord"` | | `webhook_url` | Discord webhook URL from your server | `"https://discord.com/api/webhooks/123/abc..."` | | `sales_reporting` | Whether to report gross Revenue or net Proceeds | `"Revenue"` or `"Proceeds"` | ### Optional Settings | Field | Description | Default | | ------------------------- | ------------------------------------------------------ | --------------------------- | | `event_type` | Filter which events to send | `"All Subscription Events"` | | `anonymous_user_behavior` | How to handle events from users without an app user ID | `"send"` | | `eventNameMappings` | Custom mapping to rename default event titles | None | ### Example Configuration ```json { "integration_id": "discord", "webhook_url": "https://discord.com/api/webhooks/1234567890/abcdefghijklmnop", "sales_reporting": "Revenue", "event_type": "All Subscription Events", "anonymous_user_behavior": "send", "eventNameMappings": { "sw_subscription_start": "New Premium Member!", "sw_trial_start": "New Trial Started" } } ``` ## Creating a Discord Webhook 1. Open your Discord server 2. Go to **Server Settings** (click the server name → Settings) 3. Navigate to **Integrations** → **Webhooks** 4. Click **New Webhook** 5. Configure the webhook: * **Name**: Choose a name (e.g., "Superwall Events") * **Channel**: Select the channel where notifications will appear * **Avatar**: Optionally customize the webhook's avatar 6. Click **Copy Webhook URL** 7. Paste the URL into your integration configuration **Tip**: Create a dedicated channel like `#subscription-events` or `#revenue-alerts` to keep notifications organized. ## Event Filtering The `event_type` setting controls which events are sent to Discord: ### All Subscription Events (Default) Sends every subscription lifecycle event: * Trial starts and conversions * New subscriptions * Renewals * Cancellations and expirations * Billing issues * Product changes * Refunds **Best for**: Teams that want complete visibility into all subscription activity. ### Revenue Events Only Only sends events with non-zero revenue: * New paid subscriptions * Renewals * Trial conversions * One-time purchases * Refunds (negative revenue) **Skips**: Trial starts, cancellations, expirations, billing issues (unless they have revenue attached). **Best for**: Teams focused on revenue notifications without the noise of non-revenue events. ## Message Format Discord messages are sent as rich embeds with the following structure: ### Embed Structure ``` ┌─────────────────────────────────────────┐ │ [Superwall Logo] Superwall │ ← Author ├─────────────────────────────────────────┤ │ 💰 New Subscriber │ ← Title (with emoji) │ │ │ $9.99 subscription started from │ ← Description │ United States │ ├─────────────────────────────────────────┤ │ 👤 User 🎯 Product 📱 Store │ ← Fields (inline) │ user_123 com.app.pro App Store • │ │ United States │ │ │ │ 💰 Revenue │ ← Revenue field │ $9.99 │ ├─────────────────────────────────────────┤ │ Powered by Superwall Jan 15, 2024 │ ← Footer + Timestamp └─────────────────────────────────────────┘ ``` ### Embed Fields | Field | Description | When Shown | | ------------------------ | -------------------------- | ----------------------- | | 👤 User | User ID or "Anonymous" | Always | | 🎯 Product | Product identifier | Always | | 📱 Store | Store name and country | Always | | 💰 Revenue / 💵 Proceeds | Formatted amount | When price ≠ 0 | | ⚙️ Sandbox | Test environment indicator | Sandbox events only | | 🎁 Offer | Promotional offer code | When offer code present | | 🔄 Product Change | Old → New product | Product change events | ## Event Titles and Colors Each event type has a distinct emoji, title, and color for quick visual identification. ### Color Coding | Color | Hex Code | Meaning | | ------ | --------- | ----------------------------------------------------- | | Green | `#36A64F` | Revenue events (purchases, renewals, conversions) | | Blue | `#3498DB` | Trial events (non-revenue) | | Red | `#FA6A6A` | Negative events (cancellations, refunds, expirations) | | Orange | `#FF9500` | Billing issues | | Purple | `#9B59B6` | Product changes | | Gray | `#666666` | Other events | ### Event Title Reference #### Trial Events | Event | Title | Color | | ----------------- | -------------------- | ----- | | Trial Start | 🤩 Trial Start | Blue | | Trial Conversion | 💰 Trial Conversion | Green | | Trial Cancelled | 😞 Cancelled Trial | Red | | Trial Refunded | 🤬 Refunded Trial | Red | | Trial Expired | 😞 Expired Trial | Red | | Trial Uncancelled | 🤩 Trial Uncancelled | Blue | #### Intro Offer Events | Event | Title | Color | | ------------------ | ------------------------- | ----- | | Intro Start (free) | 🤩 Intro Offer Start | Blue | | Intro Start (paid) | 💰 Intro Offer Start | Green | | Intro Conversion | 💰 Intro Offer Conversion | Green | | Intro Cancelled | 😞 Cancelled Intro Offer | Red | | Intro Refunded | 🤬 Refunded Intro Offer | Red | #### Subscription Events | Event | Title | Color | | ---------------- | --------------------------- | ----- | | New Subscription | 💰 New Subscriber | Green | | Renewal | 💰 Renewal | Green | | Cancellation | 😞 Cancelled Subscription | Red | | Refund | 🤬 Refunded Subscription | Red | | Expiration | 😞 Expired Subscription | Red | | Uncancellation | 🤩 Subscription Uncancelled | Green | #### Other Events | Event | Title | Color | | ------------------- | ---------------------- | ------ | | One-Time Purchase | 💰 One-Time Purchase | Green | | Product Change | 😵‍💫 Product Change | Purple | | Billing Issue | 🫠 Billing Issue | Orange | | Subscription Paused | ⏸️ Subscription Paused | Gray | ## Revenue Display ### Revenue vs Proceeds The `sales_reporting` setting controls which amount is displayed: * **Revenue**: The full price charged to the customer (e.g., $9.99) * **Proceeds**: The amount after store fees (e.g., $8.49 after Apple's 15-30% commission) The field label changes based on your setting: * Revenue mode: "💰 Revenue" * Proceeds mode: "💵 Proceeds" ### Currency Formatting Amounts are automatically formatted with the correct currency symbol and locale: * `$9.99` for USD * `€9.99` for EUR * `£9.99` for GBP * `¥999` for JPY ### Zero-Value Events Events without revenue (trial starts, cancellations, expirations) do not show a revenue field, keeping the message compact. ### Refunds Refunds display negative amounts: * "💰 Revenue: -$9.99" ## Anonymous User Handling The `anonymous_user_behavior` setting controls how events from unidentified users are handled: ### Send (Default) * Events from anonymous users are sent to Discord * User field displays "Anonymous" * Useful for complete visibility into all subscription activity ### Don't Send * Events from anonymous users are skipped * No notification is sent to Discord * Useful if you only want to track identified users ## Sandbox Events Events from sandbox/test environments are clearly marked: * A "⚙️ Sandbox" field is added with value "Test Environment" * Helps distinguish test events from production activity * Production events do not show any environment indicator ## Custom Event Names Use `eventNameMappings` to customize event titles: ```json { "eventNameMappings": { "sw_trial_start": "🎉 New Trial User!", "sw_subscription_start": "💎 VIP Member Joined", "sw_renewal": "🔄 Subscription Renewed", "sw_subscription_cancelled": "👋 Member Churned" } } ``` ### Available Event Keys | Key | Default Title | | --------------------------- | ------------------------- | | `sw_trial_start` | 🤩 Trial Start | | `sw_trial_converted` | 💰 Trial Conversion | | `sw_trial_cancelled` | 😞 Cancelled Trial | | `sw_subscription_start` | 💰 New Subscriber | | `sw_renewal` | 💰 Renewal | | `sw_subscription_cancelled` | 😞 Cancelled Subscription | | `sw_subscription_expired` | 😞 Expired Subscription | | `sw_refund` | 🤬 Refunded Subscription | | `sw_billing_issue` | 🫠 Billing Issue | | `sw_product_change` | 😵‍💫 Product Change | | `sw_non_renewing_purchase` | 💰 One-Time Purchase | ## Testing the Integration ### 1. Validate Credentials The integration validates your webhook URL by sending a test event. If the URL is invalid or the webhook has been deleted, validation will fail. ### 2. Send a Test Event Trigger a subscription event from your app (or use sandbox mode) to verify messages appear correctly. ### 3. Verify in Discord Check your configured channel for the notification: * Confirm the embed appears with correct formatting * Verify colors match the event type * Check that fields display correct information ### 4. Test Scenarios * [ ] New subscription shows green with 💰 emoji * [ ] Trial start shows blue with 🤩 emoji * [ ] Cancellation shows red with 😞 emoji * [ ] Revenue field shows correct amount * [ ] Sandbox events show "⚙️ Sandbox" field * [ ] Revenue-only filter skips zero-price events * [ ] Anonymous users show "Anonymous" or are skipped per setting * [ ] Custom event names appear in title ## Best Practices 1. **Create a dedicated channel**: Keep subscription notifications separate from general chat to avoid noise and make monitoring easier. 2. **Use Revenue Events Only for busy apps**: If you have high volume, filtering to revenue-only events reduces noise while keeping you informed of important transactions. 3. **Set up channel notifications**: Configure Discord channel notification settings (e.g., only notify for @mentions) to avoid constant pings. 4. **Consider multiple webhooks**: Create separate webhooks for different event types (e.g., one for revenue in `#sales`, one for all events in `#subscription-logs`). 5. **Monitor billing issues**: Pay special attention to orange "🫠 Billing Issue" notifications—these represent potential revenue at risk. 6. **Use meaningful custom names**: If you customize event names, make them clear and actionable for your team. ## Common Use Cases ### Sales Celebration Channel Create a `#sales` channel with revenue-only events: ```json { "event_type": "Revenue Events Only", "sales_reporting": "Revenue" } ``` Celebrate new subscribers and renewals with your team! ### Churn Monitoring Create a `#churn-alerts` channel and filter to cancellation events using a separate integration instance: * Monitor cancellation patterns * Quickly identify if something is causing unusual churn * React to billing issues before they become cancellations ### Customer Success Integration Use the user ID field to quickly look up users in your CRM or support system: * Click the dashboard URL in the embed to view user details * Reach out proactively to users who cancelled * Thank high-value subscribers personally ### Team Revenue Dashboard Display the Discord channel on a team dashboard or TV: * Real-time visualization of subscription activity * Color-coded events make it easy to gauge health at a glance * Celebrate wins and identify issues quickly ## Troubleshooting ### Messages Not Appearing **Possible causes:** * Invalid webhook URL * Webhook was deleted in Discord * Channel permissions prevent webhook posting * Event filtered out by `event_type` setting **Solutions:** 1. Verify the webhook still exists in Server Settings → Integrations 2. Check that the webhook has permission to post in the target channel 3. Confirm `event_type` setting includes the event you're expecting 4. Re-create the webhook if it was deleted ### Webhook Rate Limited **Possible causes:** * Discord rate limits webhook requests (30 requests per minute per channel) * High volume of subscription events **Solutions:** 1. Use "Revenue Events Only" to reduce volume 2. Consider using a less busy channel 3. Events will be queued and retried automatically ### Wrong Event Names or Emojis **Possible causes:** * Custom `eventNameMappings` overriding defaults * Unexpected event type mapping **Solutions:** 1. Review your `eventNameMappings` configuration 2. Check the event title reference table above 3. Remove custom mappings to restore defaults ### Missing Revenue Field **Possible causes:** * Event has zero price (normal for trial starts, cancellations) * This is expected behavior **Solutions:** * Revenue field only appears when price ≠ 0 * Trial starts, cancellations, and expirations typically have no revenue ### Sandbox Badge Appearing **Possible causes:** * Event came from sandbox/test environment * This is expected behavior **Solutions:** * The "⚙️ Sandbox" field only appears for sandbox events * Verify you're testing in the correct environment ## Rate Limits Discord enforces rate limits on webhooks: | Limit | Value | | --------------------- | ---------------------------- | | Requests per minute | 30 per channel | | Embed limit | 10 embeds per message | | Total character limit | 6,000 characters per message | The integration sends one embed per event, which is well within these limits. For extremely high-volume applications, consider using the "Revenue Events Only" filter. ## API Reference ### Endpoint Events are sent directly to your Discord webhook URL: ``` POST https://discord.com/api/webhooks/{webhook_id}/{webhook_token} ``` ### Request Headers ``` Content-Type: application/json ``` ### Request Body ```json { "embeds": [ { "author": { "name": "Superwall", "icon_url": "https://superwall.com/favicon.ico" }, "title": "💰 New Subscriber", "description": "$9.99 subscription started from United States", "url": "https://superwall.com/applications/{app_id}", "color": 3582031, "thumbnail": { "url": "https://superwall.com/favicon.ico" }, "fields": [ { "name": "👤 User", "value": "user_123", "inline": true }, { "name": "🎯 Product", "value": "com.app.premium", "inline": true }, { "name": "📱 Store", "value": "App Store • United States", "inline": true }, { "name": "💰 Revenue", "value": "$9.99", "inline": true } ], "timestamp": "2024-01-15T10:30:00.000Z", "footer": { "text": "Powered by Superwall", "icon_url": "https://superwall.com/favicon.ico" } } ] } ``` ### Response **Success**: `204 No Content` (Discord returns no body on success) **Error**: * `400 Bad Request`: Invalid embed structure * `401 Unauthorized`: Invalid webhook token * `404 Not Found`: Webhook was deleted * `429 Too Many Requests`: Rate limited --- # Statsig Source: https://superwall.com/docs/integrations/statsig The Statsig integration allows you to automatically send Superwall subscription and payment events to your Statsig project. This integration provides comprehensive event tracking with user properties for experimentation and analytics. In the **Analytics** section within **Integrations**, you can connect your Statsig account to Superwall: ![](/images/integrations-statsig.jpeg) ### Required fields Fill out the following fields and **click** the **Enable Statsig** button at the bottom right to save your changes: ![](/images/integrations-config-statsig.jpeg) * **Client SDK Key:** Your Statsig client SDK key from Statsig → Project Settings → Keys & Environments. * **Environment:** Which environments to send events from. * **Sales Reporting:** Whether to report Proceeds after store taxes & fees or Revenue. Choose between **Proceeds** (after store taxes & fees) or **Revenue**. ### Features * **Automatic Event Mapping**: Converts Superwall events to Statsig-friendly event names with `sw_` prefix * **Revenue Tracking**: Tracks both price (gross) and proceeds (net after fees) * **User Property Enrichment**: Attaches store, product, and transaction metadata to user objects * **Environment Tier Separation**: Uses Statsig's tier system to separate production and staging data * **Sandbox Isolation**: Separate tracking for sandbox events * **Transaction ID Tracking**: Maintains transaction IDs as custom IDs for reconciliation * **Custom Event Metadata**: Includes all Superwall event data as metadata for deep analysis ### Configuration #### Required settings | Field | Description | Example | | ----------------- | -------------------------------------- | ------------------------------------------ | | `client_sdk_key` | Your Statsig client SDK key | `"client-abc123def456..."` | | `environment` | Which environments to send events from | `"Production"` or `"Production & Sandbox"` | | `sales_reporting` | Which value to report | `"Revenue"` or `"Proceeds"` | ### Event mapping Superwall events are transformed into standardized Statsig events with the `sw_` prefix: #### Trial events | Superwall Event | Statsig Event | Description | | ---------------------------------------- | ---------------------- | ----------------------- | | `initial_purchase` + `periodType: TRIAL` | `sw_trial_start` | Trial period begins | | `cancellation` + `periodType: TRIAL` | `sw_trial_cancelled` | Trial cancelled | | `uncancellation` + `periodType: TRIAL` | `sw_trial_uncancelled` | Trial reactivated | | `expiration` + `periodType: TRIAL` | `sw_trial_expired` | Trial ended | | `renewal` + `isTrialConversion: true` | `sw_trial_converted` | Trial converted to paid | #### Intro offer events | Superwall Event | Statsig Event | Description | | ---------------------------------------- | ---------------------------- | -------------------------- | | `initial_purchase` + `periodType: INTRO` | `sw_intro_offer_start` | Intro offer begins | | `cancellation` + `periodType: INTRO` | `sw_intro_offer_cancelled` | Intro offer cancelled | | `uncancellation` + `periodType: INTRO` | `sw_intro_offer_uncancelled` | Intro offer reactivated | | `expiration` + `periodType: INTRO` | `sw_intro_offer_expired` | Intro offer ended | | `renewal` + `periodType: INTRO` | `sw_intro_offer_converted` | Intro converted to regular | #### Subscription events | Superwall Event | Statsig Event | Description | | ----------------------------------------- | ----------------------------- | ------------------------ | | `initial_purchase` + `periodType: NORMAL` | `sw_subscription_start` | Subscription begins | | `renewal` + `periodType: NORMAL` | `sw_renewal` | Subscription renewed | | `cancellation` + `periodType: NORMAL` | `sw_subscription_cancelled` | Subscription cancelled | | `uncancellation` + `periodType: NORMAL` | `sw_subscription_uncancelled` | Subscription reactivated | | `expiration` + `periodType: NORMAL` | `sw_subscription_expired` | Subscription ended | | `subscription_paused` | `sw_subscription_paused` | Subscription paused | | `billing_issue` | `sw_billing_issue` | Payment failed | #### Other events | Superwall Event | Statsig Event | Description | | -------------------------- | -------------------------- | ----------------- | | `product_change` | `sw_product_change` | Plan changed | | `non_renewing_purchase` | `sw_non_renewing_purchase` | One-time purchase | | Any event with `price < 0` | `sw_refund` | Refund processed | ### Event properties Every Statsig event includes the following structure: #### Core event fields * `eventName`: The mapped event name with `sw_` prefix * `value`: Revenue amount (when applicable) * `time`: Unix timestamp in milliseconds * `user`: User object with identity and properties * `metadata`: All webhook data fields #### User object The user object contains: * **userID**: User identifier (uses `originalAppUserId` or falls back to `originalTransactionId`) * **country**: Two-letter country code (e.g., "US", "GB") * **custom**: Transaction properties attached to the user * `isFamilyShare`: Whether the latest transaction is a family share * `store`: The store of the latest transaction (APP\_STORE, PLAY\_STORE, STRIPE, PADDLE) * `productId`: The product ID of the latest transaction * `bundleId`: The bundle ID of the latest transaction * **customIDs**: Additional identifiers * `originalTransactionId`: Store transaction ID (when available) * **statsigEnvironment**: Environment tier configuration * `tier: "production"` for production events * `tier: "staging"` for sandbox events #### Event metadata All fields from the webhook are included as metadata: * `id`, `name`, `cancelReason`, `exchangeRate` * `isSmallBusiness`, `periodType`, `countryCode` * `price`, `proceeds`, `priceInPurchasedCurrency` * `taxPercentage`, `commissionPercentage`, `takehomePercentage` * `offerCode`, `isFamilyShare`, `expirationAt` * `transactionId`, `originalTransactionId`, `originalAppUserId` * `store`, `purchasedAt`, `currencyCode`, `productId` * `environment`, `isTrialConversion`, `newProductId` * `bundleId`, `ts` ### Revenue reporting options #### Price vs proceeds The `sales_reporting` setting determines which value is used for the `value` field: | Setting | Value Used | Description | | ------------ | ---------- | ----------------------------------------- | | `"Revenue"` | `price` | Gross revenue before store fees and taxes | | `"Proceeds"` | `proceeds` | Net revenue after store fees and taxes | #### Examples **Gross Revenue (Price):** * Transaction price: $9.99 * Store commission (30%): $3.00 * Your proceeds: $6.99 * Reported to Statsig: **$9.99** **Net Revenue (Proceeds):** * Transaction price: $9.99 * Store commission (30%): $3.00 * Your proceeds: $6.99 * Reported to Statsig: **$6.99** ### Sandbox handling #### With sandbox enabled If `environment` is set to `"Production & Sandbox"`: * Production events → Tagged with `tier: "production"` * Sandbox events → Tagged with `tier: "staging"` #### Without sandbox enabled If `environment` is set to `"Production"`: * Production events → Tagged with `tier: "production"` * Sandbox events → **Skipped** (not sent to Statsig) This allows you to: * Filter events by environment tier in Statsig dashboards * Create separate metrics for production vs. sandbox * Validate integration without polluting production data ### Refund handling Refunds are automatically detected when `price < 0`: * Event type: `sw_refund` * Value field: Negative amount * All metadata preserved for analysis Example: * Original purchase: +$9.99 * Refund event: -$9.99 * Net effect on metrics: $0.00 ### User identification The integration uses the following hierarchy for user identification: 1. **Primary**: `originalAppUserId` (if available) 2. **Fallback**: `originalTransactionId` (always present) This ensures consistent user tracking even for: * Legacy users without app user IDs * Family sharing scenarios * Cross-platform subscriptions ### Testing the integration #### 1. Trigger sandbox events * iOS: Use TestFlight with a sandbox Apple ID. StoreKit Configuration files do not generate App Store Server Notifications, so webhooks and downstream integrations won't fire. * Google Play: Use license test accounts to perform test purchases in sandbox. * Stripe: Use Stripe Test Mode to create sandbox transactions. #### 2. Verify in Statsig Check your Statsig project: 1. Navigate to **Metrics** → **Events Stream** 2. Look for events with `sw_` prefix 3. Click on an event to view properties and metadata 4. Verify the `statsigEnvironment.tier` matches your configuration ### Troubleshooting #### Events not appearing 1. **Check API Key**: Ensure you're using the client SDK key (starts with "client-") 2. **Check Environment**: Confirm sandbox events are enabled if testing with sandbox data 3. **Check Events Stream**: Look in Metrics → Events Stream, not just dashboards 4. **Wait for Processing**: Events may take a few seconds to appear #### Authentication errors * **Invalid Key Format**: Client SDK keys must start with "client-" * **Wrong Project**: Verify the key belongs to the correct Statsig project * **Key Permissions**: Ensure the key has event logging permissions #### Missing or incorrect data * **Check Event Properties**: Use Statsig's Events Stream to inspect raw event data * **Verify User ID**: Ensure `originalAppUserId` is being set in your app * **Environment Mismatch**: Production events won't appear if filtered for staging tier ### Best practices 1. **Use Consistent User IDs**: Send the same user IDs to both Superwall and Statsig for proper correlation 2. **Choose Revenue Model**: Decide between gross (Revenue) vs net (Proceeds) and use consistently 3. **Set Up Environment Tiers**: Use staging tier for testing without affecting production metrics 4. **Monitor Events Stream**: Regularly check the Events Stream for data quality 5. **Create Custom Metrics**: Build metrics based on subscription events for experimentation 6. **Handle Refunds**: Account for negative revenue events in your analysis ### Rate limits Statsig has the following limits: * **Events**: 10,000 requests/second per project * **Batch Size**: 500 events per batch (this integration sends one at a time) * **Request Size**: 1MB maximum per request The integration sends events individually, well within these limits. ### Data privacy * **PII Handling**: User IDs are pseudonymous by default * **HTTPS Only**: All events sent over encrypted connections * **Data Retention**: Follows your Statsig project settings * **Deletion Requests**: Handle via Statsig's privacy tools --- # Firebase Source: https://superwall.com/docs/integrations/firebase The Firebase integration automatically sends Superwall subscription and payment events to Firebase Analytics (Google Analytics 4) using the Measurement Protocol. Track subscription lifecycle events, analyze revenue metrics, and leverage Firebase's powerful analytics capabilities. The Firebase integration automatically sends Superwall subscription and payment events to Firebase Analytics (Google Analytics 4) using the Measurement Protocol. Track subscription lifecycle events, analyze revenue metrics, and leverage Firebase's powerful analytics capabilities with automatic event mapping and ecommerce tracking. ## Features * **Standard Ecommerce Events**: Uses Firebase's standard `purchase` and `refund` events for revenue tracking * **Measurement Protocol**: Direct server-side integration via Google Analytics Measurement Protocol * **Revenue Tracking**: Automatic revenue attribution with ecommerce parameters * **Sandbox Isolation**: Separate tracking for production and sandbox events * **Custom Event Mapping**: Non-revenue events mapped to custom Firebase events * **Cross-Platform User Tracking**: Optional `user_id` for cross-device analysis * **Debug Validation**: Built-in validation via Firebase's debug endpoint * **Platform Attribution**: Tracks which store (App Store, Play Store, Stripe) generated revenue ## Configuration Firebase requires separate credentials for iOS and Android apps since each platform has its own data stream in Firebase Analytics. You can configure one or both platforms depending on your app. ### iOS Settings | Field | Description | Example | | ----------------------------- | -------------------------------------------------------------------------------------------------------------------- | -------------------------------- | | `ios_firebase_app_id` | iOS Firebase App ID from Firebase Console → Project Settings → Your Apps → iOS App ID | `"1:123456789:ios:abc123def456"` | | `ios_api_secret` | iOS API Secret from Firebase Console → Google Analytics → Admin → iOS Data Stream → Measurement Protocol API secrets | `"AbCdEfGhIjKlMnOp"` | | `sandbox_ios_firebase_app_id` | Optional: iOS Firebase App ID for sandbox events | `"1:123456789:ios:xyz789"` | | `sandbox_ios_api_secret` | Optional: iOS API Secret for sandbox events | `"QrStUvWxYz123456"` | ### Android Settings | Field | Description | Example | | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | | `android_firebase_app_id` | Android Firebase App ID from Firebase Console → Project Settings → Your Apps → Android App ID | `"1:123456789:android:def456abc123"` | | `android_api_secret` | Android API Secret from Firebase Console → Google Analytics → Admin → Android Data Stream → Measurement Protocol API secrets | `"ZyXwVuTsRqPo9876"` | | `sandbox_android_firebase_app_id` | Optional: Android Firebase App ID for sandbox events | `"1:123456789:android:sandbox123"` | | `sandbox_android_api_secret` | Optional: Android API Secret for sandbox events | `"SandboxSecret1234"` | ### Common Settings | Field | Description | Example | | ------------------------- | ----------------------------------------- | --------------------------- | | `sales_reporting` | Which value to report | `"Revenue"` or `"Proceeds"` | | `anonymous_user_behavior` | How to handle events from anonymous users | `"send"` or `"dontSend"` | > **Note**: At least one platform (iOS or Android) must have both `firebase_app_id` and `api_secret` configured. Events from platforms without credentials will be skipped. ### Example Configuration (Both Platforms) ```json { "ios_firebase_app_id": "1:123456789012:ios:abcdef1234567890", "ios_api_secret": "your_ios_api_secret", "sandbox_ios_firebase_app_id": "1:123456789012:ios:sandbox1234567890", "sandbox_ios_api_secret": "your_ios_sandbox_api_secret", "android_firebase_app_id": "1:123456789012:android:fedcba0987654321", "android_api_secret": "your_android_api_secret", "sandbox_android_firebase_app_id": "1:123456789012:android:sandbox0987654321", "sandbox_android_api_secret": "your_android_sandbox_api_secret", "sales_reporting": "Revenue", "anonymous_user_behavior": "send" } ``` ### Example Configuration (iOS Only) ```json { "ios_firebase_app_id": "1:123456789012:ios:abcdef1234567890", "ios_api_secret": "your_ios_api_secret", "sales_reporting": "Revenue" } ``` ## App Instance ID Requirement **Critical**: The Firebase integration requires `firebaseAppInstanceId` to be set in the user's `userAttributes` from your client app. This is the unique installation identifier from the Firebase Analytics SDK. ### How to Set It Up 1. In your app, retrieve the Firebase App Instance ID: **iOS (Swift):** ```swift import FirebaseAnalytics Analytics.appInstanceID { appInstanceId, error in if let appInstanceId = appInstanceId { Superwall.shared.setUserAttributes([ "firebaseAppInstanceId": appInstanceId ]) } } ``` **Android (Kotlin):** ```kotlin import com.google.firebase.analytics.FirebaseAnalytics FirebaseAnalytics.getInstance(context).appInstanceId.addOnSuccessListener { appInstanceId -> Superwall.instance.setUserAttributes(mapOf( "firebaseAppInstanceId" to appInstanceId )) } ``` ### What Happens Without It If `firebaseAppInstanceId` is not found in `userAttributes`: * The event is **skipped** (not sent to Firebase) ### Revenue Events (Standard Ecommerce) Events with non-zero amounts use Firebase's standard ecommerce events for proper revenue tracking: | Condition | Firebase Event | Description | | ----------- | -------------- | -------------------------------------- | | `price > 0` | `purchase` | Purchase/renewal with positive revenue | | `price < 0` | `refund` | Refund with negative revenue | ### Non-Revenue Events (Custom Events) Events without revenue are mapped to custom Firebase events (lowercase, underscores): | Superwall Event | Firebase Event | Description | | ---------------------------- | ----------------------------- | ------------------------- | | `initial_purchase` + TRIAL | `trial_start` | Trial begins | | `initial_purchase` + INTRO | `intro_offer_start` | Intro offer begins | | `initial_purchase` + NORMAL | `subscription_start` | Paid subscription begins | | `renewal` + trial conversion | `trial_conversion` | Trial converts to paid | | `renewal` + INTRO | `intro_offer_conversion` | Intro converts to regular | | `renewal` + NORMAL | `subscription_renewal` | Regular renewal | | `cancellation` + TRIAL | `trial_cancellation` | Trial cancelled | | `cancellation` + INTRO | `intro_offer_cancellation` | Intro cancelled | | `cancellation` + NORMAL | `subscription_cancellation` | Subscription cancelled | | `uncancellation` + TRIAL | `trial_uncancellation` | Trial reactivated | | `uncancellation` + INTRO | `intro_offer_uncancellation` | Intro reactivated | | `uncancellation` + NORMAL | `subscription_uncancellation` | Subscription reactivated | | `expiration` + TRIAL | `trial_expiration` | Trial ended | | `expiration` + INTRO | `intro_offer_expiration` | Intro ended | | `expiration` + NORMAL | `subscription_expiration` | Subscription ended | | `billing_issue` | `billing_issue` | Payment failed | | `subscription_paused` | `subscription_paused` | Subscription paused | | `product_change` | `product_change` | Plan changed | | `non_renewing_purchase` | `non_renewing_purchase` | One-time purchase | | `test` | `test` | Test event | ## Event Parameters ### Required Parameters (All Events) Every Firebase event includes these parameters: | Parameter | Description | Example | | ---------------------- | ------------------------------------------------------- | --------------- | | `session_id` | Event timestamp (required for Firebase Console display) | `1699876543000` | | `engagement_time_msec` | Engagement time (required for Firebase Console display) | `100` | | `store` | Payment source | `"APP_STORE"` | | `environment` | Production or Sandbox | `"Production"` | | `country_code` | User's country | `"US"` | | `period_type` | Subscription period type | `"NORMAL"` | ### Ecommerce Parameters (Revenue Events) Purchase and refund events include additional ecommerce fields: | Parameter | Description | Example | | ---------------- | ----------------------------------- | -------------------- | | `currency` | ISO 4217 currency code | `"USD"` | | `value` | Transaction amount (absolute value) | `9.99` | | `transaction_id` | Unique transaction identifier | `"1000000123456789"` | | `coupon` | Offer code (if applicable) | `"SUMMER2024"` | | `items` | Array of purchased items | See below | **Items Array Structure:** ```json { "items": [ { "item_id": "com.example.premium_monthly", "item_name": "com.example.premium_monthly", "price": 9.99, "quantity": 1 } ] } ``` ### Non-Revenue Event Parameters Non-revenue events include: | Parameter | Description | Example | | ---------------- | ---------------------- | ----------------------- | | `transaction_id` | Transaction identifier | `"1000000123456789"` | | `product_id` | Product identifier | `"com.example.premium"` | ## Revenue Tracking ### Automatic Revenue Attribution Revenue is tracked using Firebase's standard ecommerce events: * **Positive revenue**: `purchase` event with positive `value` * **Negative revenue**: `refund` event with positive `value` (Firebase expects absolute values) * **Zero revenue**: Custom event (no ecommerce parameters) ### Revenue Reporting Options The `sales_reporting` setting determines which value is used: | Setting | Value Used | Description | | ------------ | ---------- | ------------------------------- | | `"Revenue"` | `price` | Gross revenue before store fees | | `"Proceeds"` | `proceeds` | Net revenue after store fees | ### Revenue Examples **Initial Purchase ($9.99):** ```json { "name": "purchase", "params": { "currency": "USD", "value": 9.99, "transaction_id": "1000000123456789", "items": [ { "item_id": "com.example.premium", "item_name": "com.example.premium", "price": 9.99, "quantity": 1 } ], "session_id": "1699876543000", "engagement_time_msec": 100, "store": "APP_STORE", "environment": "Production" } } ``` **Refund (-$9.99):** ```json { "name": "refund", "params": { "currency": "USD", "value": 9.99, "transaction_id": "1000000123456789", "items": [ { "item_id": "com.example.premium", "item_name": "com.example.premium", "price": 9.99, "quantity": 1 } ], "session_id": "1699876543000", "engagement_time_msec": 100, "store": "APP_STORE", "environment": "Production" } } ``` ### Platform Tracking & Automatic Credential Selection The integration automatically selects the correct Firebase credentials based on the `store` field in each event: | Store | Platform | Credentials Used | | ------------ | ------------- | ------------------------------------------------ | | `APP_STORE` | iOS | `ios_firebase_app_id` + `ios_api_secret` | | `PLAY_STORE` | Android | `android_firebase_app_id` + `android_api_secret` | | `STRIPE` | iOS (default) | `ios_firebase_app_id` + `ios_api_secret` | | `PADDLE` | iOS (default) | `ios_firebase_app_id` + `ios_api_secret` | > **Note**: If credentials are not configured for a platform, events from that platform will be skipped. For example, if you only configure iOS credentials, Play Store events will be skipped. ### With Platform Sandbox Credentials If sandbox credentials are configured for a platform: * Production events → Production Firebase project (using production credentials) * Sandbox events → Sandbox Firebase project (using sandbox credentials) **Example for iOS:** * iOS production event → Uses `ios_firebase_app_id` + `ios_api_secret` * iOS sandbox event → Uses `sandbox_ios_firebase_app_id` + `sandbox_ios_api_secret` ### Without Platform Sandbox Credentials If sandbox credentials are NOT provided for a platform: * Production events → Production Firebase project * Sandbox events → **Skipped** (not sent) This behavior is per-platform, so: * You can have iOS sandbox credentials but skip Android sandbox events * You can configure sandbox for Android but not iOS * Each platform is independent This prevents test data from polluting production analytics. ## Testing the Integration ### 1. Validate Credentials The integration validates settings using Firebase's debug endpoint: * Sends a test event to Firebase * Validates event format and structure * **Important**: The debug step does NOT validate `api_secret` or `firebase_app_id` ### 2. Verify in Firebase Console After sending events, verify in Firebase: 1. **DebugView**: Firebase Console → Analytics → DebugView (for real-time debugging) 2. **Events**: Firebase Console → Analytics → Events (may take up to 24 hours) 3. **Revenue**: Firebase Console → Analytics → Revenue (for purchase/refund events) ## Best Practices 1. **Set App Instance ID Early**: Call `setUserAttributes` with `firebaseAppInstanceId` as soon as the app launches to ensure all subscription events are tracked. 2. **Separate Environments**: Use separate Firebase projects (or at minimum, separate measurement streams) for sandbox and production to keep analytics clean. 3. **Revenue Model Consistency**: Choose gross (`Revenue`) vs net (`Proceeds`) consistently and document your choice for reporting alignment. 4. **Enable DebugView**: During testing, enable Firebase DebugView on your test device to see events in real-time. 5. **Use User ID**: Set `originalAppUserId` in Superwall to enable cross-device user tracking in Firebase Analytics. ## Common Use Cases ### Revenue Analytics ``` Event: purchase Breakdown by: store, country_code, product_id (via items) Metric: Sum of value ``` ### Conversion Funnel ``` 1. trial_start 2. purchase (trial conversion) Conversion Rate: Step 2 / Step 1 ``` ### Churn Analysis ``` Events: subscription_cancellation, subscription_expiration Segment by: period_type, store ``` ### LTV Calculation ``` Event: purchase Group by: user_id Calculate: Sum of value per user ``` ## Troubleshooting ### Events Not Appearing in Firebase 1. **Check App Instance ID**: Ensure `firebaseAppInstanceId` is set in `userAttributes` 2. **Verify Platform Credentials**: Confirm the correct platform credentials are configured: * iOS events (App Store) require `ios_firebase_app_id` + `ios_api_secret` * Android events (Play Store) require `android_firebase_app_id` + `android_api_secret` 3. **Check Environment**: Sandbox events require sandbox credentials for that platform 4. **Wait for Processing**: Events may take up to 24 hours to appear in standard reports (use DebugView for real-time) ### Events Skipped Due to Missing Platform Credentials **Problem**: Events are being skipped with warning "No \[platform] credentials configured" **Solutions**: 1. **iOS events skipped**: Add `ios_firebase_app_id` and `ios_api_secret` 2. **Android events skipped**: Add `android_firebase_app_id` and `android_api_secret` 3. **Sandbox events skipped**: Add sandbox credentials for the specific platform (e.g., `sandbox_ios_firebase_app_id` + `sandbox_ios_api_secret`) **Single-Platform Apps**: If your app is iOS-only or Android-only, you only need to configure credentials for that platform. Events from unconfigured platforms will be skipped (this is expected behavior). ### Missing firebaseAppInstanceId **Problem**: Events are being skipped with warning about missing `firebaseAppInstanceId` **Solutions**: 1. Ensure your app calls `FirebaseAnalytics.getAppInstanceId()` and passes it to Superwall 2. Verify `setUserAttributes` is called before any purchases occur 3. Check that the attribute key is exactly `firebaseAppInstanceId` (case-sensitive) ### Revenue Not Tracking 1. **Check Event Type**: Only `purchase` and `refund` events track revenue 2. **Check Amount**: Zero amounts don't create revenue events 3. **Check Currency**: Ensure `currencyCode` is present in the webhook data 4. **Check Firebase Reports**: Revenue appears in Analytics → Revenue (not Events) ### Debug Validation Errors **Problem**: Credential validation returns errors **Common Causes**: * Invalid event parameter names (must be alphanumeric with underscores) * Missing required parameters (`session_id`, `engagement_time_msec`) * Invalid `app_instance_id` format **Note**: The debug endpoint validates event format but cannot validate whether your `api_secret` or `firebase_app_id` are correct. You must verify events appear in Firebase Console. ### Duplicate Events Firebase handles deduplication via `transaction_id`: * Same `transaction_id` within 72 hours is deduplicated * Ensure unique transaction IDs for each event ## Rate Limits Google Analytics Measurement Protocol limits: | Limit | Value | | ------------------------- | -------------------------------- | | Events per request | 25 (we send 1 at a time) | | Requests per user per day | No hard limit (fair use applies) | | Payload size | 130KB maximum | | Event name length | 40 characters | | Parameter value length | 100 characters | The integration sends one event per request, well within all limits. ## Data Privacy * **App Instance ID**: Pseudonymous device identifier * **User ID**: Optional, only sent if `originalAppUserId` is set * **Data Retention**: Follows your Firebase project settings * **Deletion**: Handle via Google Analytics User Deletion API * **GDPR**: Firebase Analytics provides data processing agreements and privacy controls * **PII**: Avoid sending PII in custom parameters --- # Slack Source: https://superwall.com/docs/integrations/slack The Slack integration sends real-time notifications about subscription events to your Slack channels. Get instant updates about new subscribers, cancellations, renewals, and revenue changes with rich, color-coded messages and contextual emojis. In the **Communication** section within **Integrations**, you can connect your Slack account to Superwall: ![](/images/integrations-slack.jpeg) ### Required Fields Fill out the following fields and **click** the **Enable Slack** button at the bottom right to save your changes: ![](/images/integrations-config-slack.jpeg) * **Webhook Url:** Your Slack webhook URL for sending messages to a channel. * **Include Sandbox:** Whether to include sandbox events in Slack notifications. * **Event Type:** Type of events to send: revenue only or all lifecycle (includes trials, cancellations). ### Features * **Real-time Notifications**: Instant Slack messages for subscription events * **Smart Filtering**: Choose between revenue events only or all subscription lifecycle events * **Visual Design**: Color-coded messages with contextual emojis for quick scanning * **Revenue Insights**: See price, proceeds, and currency information at a glance * **Sandbox Control**: Optional inclusion of sandbox events * **Rich Context**: Includes user ID, product, country, and transaction details ## Configuration ### Required Settings | Field | Description | Example | | ----------------- | --------------------------------- | ------------------------------------------------------ | | `integration_id` | Must be set to `"slack"` | `"slack"` | | `webhook_url` | Your Slack incoming webhook URL | `"https://hooks.slack.com/services/..."` | | `include_sandbox` | Whether to include sandbox events | `"Production Only"` or `"Production & Sandbox"` | | `event_type` | Types of events to send | `"Revenue Events Only"` or `"All Subscription Events"` | ### Example Configuration ```json { "integration_id": "slack", "webhook_url": "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXX", "include_sandbox": "Production Only", "event_type": "Revenue Events Only" } ``` ## Setting Up Slack Webhooks 1. **Create Incoming Webhook**: * Go to [api.slack.com/apps](https://api.slack.com/apps) * Create a new app or select existing * Enable "Incoming Webhooks" * Add new webhook to workspace * Select target channel 2. **Copy Webhook URL**: * Format: `https://hooks.slack.com/services/T.../B.../...` * Keep this URL secure 3. **Verify Connection**: * Trigger a real sandbox transaction (see Testing section below) * Verify a message appears in your selected channel ## Event Filtering ### Revenue Events Only Sends notifications for events with monetary impact: * ✅ Initial purchases (paid) * ✅ Renewals * ✅ Refunds (negative amounts) * ✅ Trial/intro conversions (when paid) * ❌ Cancellations (no immediate revenue impact) * ❌ Expirations * ❌ Billing issues ### All Subscription Events Sends notifications for all lifecycle events: * ✅ All revenue events (above) * ✅ Trial starts * ✅ Cancellations * ✅ Uncancellations * ✅ Expirations * ✅ Billing issues * ✅ Product changes * ✅ Subscription pauses ## Message Format ### Visual Indicators Messages use colors and emojis for quick scanning: #### Colors * 🟢 **Green** (#36a64f): Positive events (purchases, renewals, uncancellations) * 🔴 **Red** (#FA6A6A): Negative events (cancellations, expirations, refunds) * ⚫ **Gray** (#666666): Neutral events (product changes, pauses) #### Emojis by Event Type **Trial Events:** * 🤩 Trial start * 💰 Trial conversion * 😞 Trial cancelled * 😞 Trial expired * 🤩 Trial uncancelled * 🤬 Trial refunded **Intro Offer Events:** * 💰/🤩 Intro offer start * 💰 Intro offer conversion * 😞 Intro offer cancelled * 😞 Intro offer expired * 🤩 Intro offer uncancelled * 🤬 Intro offer refunded **Subscription Events:** * 💰 New subscriber * 💰 Renewal * 😞 Subscription cancelled * 😞 Subscription expired * 🤩 Subscription uncancelled * 🤬 Subscription refunded **Special Events:** * 😵‍💫 Product change * 🫠 Billing issue * ⏸️ Subscription paused * 💸 Non-renewing purchase ### Message Structure Each Slack message includes: ``` [Emoji] [Event Description] ━━━━━━━━━━━━━━━━━━━━━ 💵 $9.99 USD (Proceeds: $6.99) 📦 com.example.premium.monthly 🌍 United States 👤 User123 🏪 APP_STORE 🔗 Transaction: 700001234567890 ``` ### Field Descriptions | Field | Description | Example | | --------------- | --------------------- | --------------------------- | | **Header** | Event type with emoji | "💰 renewal" | | **Price** | Transaction amount | "$9.99 USD" | | **Proceeds** | Net after fees | "Proceeds: $6.99" | | **Product** | Product identifier | "com.example.premium" | | **Country** | User's country | "United States" | | **User** | User identifier | "User123" or transaction ID | | **Store** | Payment provider | "APP\_STORE" | | **Transaction** | Transaction ID | "700001234567890" | ## Sandbox Handling ### Production Only * Production events → Sent to Slack * Sandbox events → **Skipped** ### Production & Sandbox * Production events → Sent to Slack * Sandbox events → Sent to Slack with 🧪 indicator Sandbox events include a note in the message to differentiate from production. ## Special Event Handling ### Refunds Identified by negative price values: * Header changes to "refunded \[type]" * Emoji changes to 🤬 * Color remains red * Shows negative amount ### Family Sharing Events with `isFamilyShare: true`: * Shows shared subscription indicator * Price may be $0 for family members * Original purchaser shows full price ### Trial Conversions Renewals with `isTrialConversion: true`: * Header shows "trial conversion" * Indicates successful trial-to-paid transition * Always green/positive color ### Product Changes Shows when users switch plans: * Displays old and new product IDs * Neutral gray color * May have $0 price ## Use Cases ### Revenue Monitoring Track real-time revenue with "Revenue Events Only": * Monitor daily subscription revenue * Get alerts for high-value purchases * Track refund activity * Celebrate trial conversions ### Customer Success Track lifecycle with "All Subscription Events": * Monitor cancellation trends * Identify billing issues quickly * Track trial-to-paid conversion * Spot at-risk subscribers ### Team Celebrations Share wins with your team: * New subscriber notifications * Trial conversion celebrations * Renewal milestones * Recovery from cancellations ## Best Practices 1. **Dedicated Channels**: Create specific channels for different event types 2. **Filter Appropriately**: Use "Revenue Only" for finance, "All Events" for customer success 3. **Include Context**: User IDs help connect events to support tickets 4. **Monitor Patterns**: Watch for unusual cancellation or refund spikes 5. **Sandbox Separation**: Consider separate webhooks for production vs testing ## Troubleshooting ### Messages Not Appearing 1. **Check Webhook URL**: Ensure URL is valid and not revoked 2. **Check Channel**: Verify bot has access to target channel 3. **Check Filters**: Confirm event type and sandbox settings 4. **Check Slack Limits**: Webhook rate limits (1 per second) ### Incorrect Information 1. **Check Timezone**: Timestamps are in UTC 2. **Check Currency**: Amounts in USD, original currency shown 3. **Check User ID**: Falls back to transaction ID if not available ### Verify the Integration 1. Trigger a sandbox transaction in your app: * iOS: Use TestFlight with a sandbox Apple ID. StoreKit Configuration files do not generate App Store Server Notifications, so webhooks and downstream integrations won't fire. * Google Play: Use license test accounts to perform sandbox purchases. * Stripe: Use Stripe Test Mode to create sandbox transactions. 2. Confirm the message arrives in the configured Slack channel. 3. If you enabled Production & Sandbox, sandbox messages include an 🧪 indicator. ## Rate Limits Slack incoming webhooks have a rate limit of 1 message per second. The integration sends events individually as they occur, typically well within this limit. ## Security Considerations * **Webhook URLs are sensitive**: Treat like passwords * **Rotate if compromised**: Generate new webhook URL if leaked * **Channel permissions**: Ensure appropriate team members have access * **PII considerations**: User IDs may be visible to channel members ## Advanced Configuration ### Multiple Channels Set up multiple integrations for different channels: * Revenue events → #revenue-alerts * Cancellations → #customer-success * All events → #subscription-monitoring ### Custom Filtering While the integration offers two preset filters, you can: * Use Slack workflows for additional filtering * Set up multiple integrations with different settings * Use Slack's notification preferences per channel ## Message Examples ### New Paid Subscription ``` 💰 new subscriber ━━━━━━━━━━━━━━━━━━━━━ 💵 $49.99 USD (Proceeds: $34.99) 📦 com.example.premium.yearly 🌍 United States 👤 user_abc123 🏪 APP_STORE 🔗 Transaction: 700001234567890 ``` ### Trial Start ``` 🤩 Trial start ━━━━━━━━━━━━━━━━━━━━━ 💵 $0.00 USD 📦 com.example.premium.monthly 🌍 Germany 👤 user_xyz789 🏪 PLAY_STORE 🔗 Transaction: GPA.1234-5678-9012 ``` ### Refund ``` 🤬 refunded subscription ━━━━━━━━━━━━━━━━━━━━━ 💵 -$9.99 USD (Proceeds: -$6.99) 📦 com.example.premium.monthly 🌍 United Kingdom 👤 user_def456 🏪 STRIPE 🔗 Transaction: sub_1234567890 ``` ### Billing Issue ``` 🫠 Billing issue ━━━━━━━━━━━━━━━━━━━━━ 💵 $0.00 USD 📦 com.example.premium.monthly 🌍 Canada 👤 user_ghi789 🏪 APP_STORE 🔗 Transaction: 700009876543210 ❗ Payment failed - subscription at risk ``` --- # Verify Webhook Requests Source: https://superwall.com/docs/integrations/webhooks-verify Learn how to verify webhook requests using the signing secret to ensure authenticity and security. ## Why Verify Webhooks? Verifying webhook requests is crucial for security. It ensures that: * Requests are actually coming from Superwall's servers * The payload hasn't been tampered with in transit * Replay attacks are prevented through timestamp validation Without verification, malicious actors could send fake webhook events to your endpoint. ## Getting Your Signing Secret Every webhook endpoint has a unique signing secret that's used to verify requests. You can find this secret in your webhook details: ![Copy webhook signing secret](/images/integration_copy_secret.jpeg) Click the **Copy Secret** button to copy your webhook's signing secret to your clipboard. Keep your signing secret secure. Never commit it to version control or expose it in client-side code. Store it as an environment variable like `SUPERWALL_WEBHOOK_SECRET`. ## Verification Methods ### Option 1: Using Svix Library (Recommended) Superwall uses [Svix](https://svix.com) for webhook delivery, which provides robust verification libraries for multiple languages. Install the Svix library: ```bash npm install svix # or yarn add svix # or pnpm add svix ``` Verify incoming requests: ```javascript import { Webhook } from 'svix'; export async function POST(request) { // Get the raw body as a string const payload = await request.text(); // Get the Svix headers const headers = { 'svix-id': request.headers.get('svix-id'), 'svix-timestamp': request.headers.get('svix-timestamp'), 'svix-signature': request.headers.get('svix-signature'), }; // Create a new Webhook instance with your secret const wh = new Webhook(process.env.SUPERWALL_WEBHOOK_SECRET); let event; try { // Verify the webhook event = wh.verify(payload, headers); } catch (err) { console.error('Webhook verification failed:', err.message); return new Response('Webhook verification failed', { status: 400 }); } // Webhook is verified - process the event console.log('Verified event:', event); // Process your event here // ... return new Response('Success', { status: 200 }); } ``` ### Option 2: Manual Verification If you prefer not to use the Svix library, you can manually verify webhooks using the HMAC signature: ```javascript import crypto from 'crypto'; function verifyWebhook(payload, headers, secret) { const msgId = headers['svix-id']; const msgTimestamp = headers['svix-timestamp']; const msgSignature = headers['svix-signature']; // Verify timestamp to prevent replay attacks const timestamp = parseInt(msgTimestamp, 10); const now = Math.floor(Date.now() / 1000); if (now - timestamp > 300) { // 5 minutes throw new Error('Webhook timestamp too old'); } // Create the signed content const signedContent = `${msgId}.${msgTimestamp}.${payload}`; // Compute the expected signature const secretBytes = Buffer.from(secret.split('_')[1], 'base64'); const signature = crypto .createHmac('sha256', secretBytes) .update(signedContent) .digest('base64'); // Compare signatures const expectedSignature = `v1,${signature}`; // Extract all signatures from the header const passedSignatures = msgSignature.split(' '); // Check if any signature matches const signatureMatch = passedSignatures.some(sig => crypto.timingSafeEqual( Buffer.from(sig), Buffer.from(expectedSignature) ) ); if (!signatureMatch) { throw new Error('Webhook signature verification failed'); } return JSON.parse(payload); } ``` ## Important Implementation Notes ### Use Raw Request Body The signature is computed against the **raw request body**. Do not parse or modify the body before verification: ```javascript // ✅ Correct - use raw body const payload = await request.text(); const event = wh.verify(payload, headers); // ❌ Wrong - parsing changes the body const payload = await request.json(); const event = wh.verify(JSON.stringify(payload), headers); // Will fail! ``` ### Required Headers Three headers are required for verification: | Header | Description | | ---------------- | ---------------------------------------- | | `svix-id` | Unique message ID | | `svix-timestamp` | Unix timestamp when the webhook was sent | | `svix-signature` | HMAC signature(s) of the message | ### Framework-Specific Examples #### Next.js (App Router) ```javascript // app/api/webhooks/route.js import { Webhook } from 'svix'; export async function POST(request) { const payload = await request.text(); const headers = { 'svix-id': request.headers.get('svix-id'), 'svix-timestamp': request.headers.get('svix-timestamp'), 'svix-signature': request.headers.get('svix-signature'), }; const wh = new Webhook(process.env.SUPERWALL_WEBHOOK_SECRET); try { const event = wh.verify(payload, headers); // Handle the event switch (event.type) { case 'initial_purchase': // Handle initial purchase break; case 'renewal': // Handle renewal break; // ... other event types } return new Response('Success', { status: 200 }); } catch (err) { return new Response('Webhook verification failed', { status: 400 }); } } ``` #### Express ```javascript import express from 'express'; import { Webhook } from 'svix'; const app = express(); // Important: Use raw body for webhook verification app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => { const payload = req.body.toString(); const headers = { 'svix-id': req.headers['svix-id'], 'svix-timestamp': req.headers['svix-timestamp'], 'svix-signature': req.headers['svix-signature'], }; const wh = new Webhook(process.env.SUPERWALL_WEBHOOK_SECRET); try { const event = wh.verify(payload, headers); // Handle the event console.log('Verified event:', event); res.status(200).send('Success'); } catch (err) { console.error('Webhook verification failed:', err.message); res.status(400).send('Verification failed'); } }); ``` #### Python (FastAPI) ```python from fastapi import FastAPI, Request, HTTPException from svix.webhooks import Webhook, WebhookVerificationError import os app = FastAPI() @app.post("/webhooks") async def handle_webhook(request: Request): payload = await request.body() headers = { "svix-id": request.headers.get("svix-id"), "svix-timestamp": request.headers.get("svix-timestamp"), "svix-signature": request.headers.get("svix-signature"), } wh = Webhook(os.environ["SUPERWALL_WEBHOOK_SECRET"]) try: event = wh.verify(payload, headers) # Handle the event print(f"Verified event: {event}") return {"status": "success"} except WebhookVerificationError as e: print(f"Webhook verification failed: {e}") raise HTTPException(status_code=400, detail="Verification failed") ``` ## Testing Webhook Verification During development, you can test webhook verification: 1. **Use the actual signing secret** from your webhook endpoint 2. **Capture real webhook payloads** by temporarily logging them 3. **Test with valid and invalid signatures** to ensure your verification works Never test with production webhooks in a development environment without proper safeguards. Consider creating a separate webhook endpoint for testing. ## Security Best Practices 1. **Always verify webhooks** - Never process unverified webhook data 2. **Use environment variables** - Store your signing secret securely 3. **Check timestamps** - Reject old webhooks to prevent replay attacks (Svix does this automatically) 4. **Return 200 quickly** - Acknowledge receipt immediately, then process asynchronously 5. **Log verification failures** - Monitor for potential attacks or configuration issues 6. **Rotate secrets periodically** - Update your signing secret if it's ever compromised ## Troubleshooting ### Verification Always Fails * Ensure you're using the **raw request body**, not a parsed/stringified version * Check that all three required headers are present * Verify you're using the correct signing secret for this webhook endpoint * Make sure your secret includes the full value (it should start with `whsec_`) ### "Timestamp too old" Errors * Your server's clock may be out of sync - verify your server time * Network delays may be too high - check your server's response time * The webhook may be a replay attack - this is working as intended ## Advanced Usage For advanced webhook verification scenarios, including signature rotation and custom verification logic, see the [Svix documentation](https://docs.svix.com/receiving/verifying-payloads/how). --- # Amplitude Source: https://superwall.com/docs/integrations/amplitude The Amplitude integration automatically sends Superwall subscription and payment events to your Amplitude project. Track subscription lifecycle events, analyze revenue metrics, and understand user behavior with automatic event mapping and revenue tracking. In the **Analytics** section within **Integrations**, you can connect your Amplitude account to Superwall: ![](/images/integrations-amplitude.jpeg) ### Required fields Fill out the following fields and **click** the **Enable Amplitude** button at the bottom right to save your changes: ![](/images/integrations-config-amplitude.jpeg) * **Region:** Data residency region for your Amplitude project. * **Api Key:** Your Amplitude API key. * **Sandbox Api Key:** Optional API key for sandbox events (leave blank to opt out). * **Sales Reporting:** Which revenue value to report in Amplitude. Choose between **Proceeds** (after store taxes & fees) or **Revenue**. ### Features * **Automatic Event Mapping**: Converts Superwall events to Amplitude-friendly format * **Revenue Tracking**: Automatic revenue attribution with LTV tracking * **Multi-Region Support**: Works with US and EU data residency * **Sandbox Isolation**: Separate tracking for production and sandbox events * **Human-Readable Events**: Events prefixed with `[Superwall]` for easy identification * **Session Tracking**: Automatic session ID generation * **Platform Attribution**: Tracks which store (App Store, Play Store, Stripe) generated revenue ### Configuration #### Required settings | Field | Description | Example | | ----------------- | ---------------------------- | --------------------------- | | `integration_id` | Must be set to `"amplitude"` | `"amplitude"` | | `region` | Data residency region | `"US (Default)"` or `"EU"` | | `api_key` | Your Amplitude API key | `"abc123def456..."` | | `sales_reporting` | Which value to report | `"Revenue"` or `"Proceeds"` | #### Optional settings | Field | Description | Example | | ----------------- | ------------------------------------------------ | ------------- | | `sandbox_api_key` | API key for sandbox events (leave blank to skip) | `"xyz789..."` | #### Example configuration ```json { "integration_id": "amplitude", "region": "US (Default)", "api_key": "your_production_api_key_here", "sandbox_api_key": "your_sandbox_api_key_here", "sales_reporting": "Revenue" } ``` ### Event mapping Superwall events are transformed into human-readable Amplitude events: #### Event name format All events are prefixed with `[Superwall]` followed by a descriptive name: * Example: `[Superwall] Trial Start` * Example: `[Superwall] Subscription Renewal` #### Complete event mapping | Superwall Event | Amplitude Event | Description | | ---------------------------- | ----------------------------------------- | ------------------------- | | `initial_purchase` + TRIAL | `[Superwall] Trial Start` | Trial begins | | `initial_purchase` + INTRO | `[Superwall] Intro Offer Start` | Intro offer begins | | `initial_purchase` + NORMAL | `[Superwall] Subscription Start` | Paid subscription begins | | `renewal` + trial conversion | `[Superwall] Trial Conversion` | Trial converts to paid | | `renewal` + INTRO | `[Superwall] Intro Offer Conversion` | Intro converts to regular | | `renewal` + NORMAL | `[Superwall] Subscription Renewal` | Regular renewal | | `cancellation` + TRIAL | `[Superwall] Trial Cancellation` | Trial cancelled | | `cancellation` + INTRO | `[Superwall] Intro Offer Cancellation` | Intro cancelled | | `cancellation` + NORMAL | `[Superwall] Subscription Cancellation` | Subscription cancelled | | `uncancellation` + TRIAL | `[Superwall] Trial Uncancellation` | Trial reactivated | | `uncancellation` + INTRO | `[Superwall] Intro Offer Uncancellation` | Intro reactivated | | `uncancellation` + NORMAL | `[Superwall] Subscription Uncancellation` | Subscription reactivated | | `expiration` + TRIAL | `[Superwall] Trial Expiration` | Trial ended | | `expiration` + INTRO | `[Superwall] Intro Offer Expiration` | Intro ended | | `expiration` + NORMAL | `[Superwall] Subscription Expiration` | Subscription ended | | `billing_issue` | `[Superwall] Billing Issue` | Payment failed | | `subscription_paused` | `[Superwall] Subscription Paused` | Subscription paused | | `product_change` | `[Superwall] Product Change` | Plan changed | | `non_renewing_purchase` | `[Superwall] Non-Renewing Purchase` | One-time purchase | | Any with `price < 0` | `[Superwall] Refund` | Refund processed | ### Event properties Every Amplitude event includes comprehensive properties: #### Core Amplitude fields * `user_id`: User identifier (uses `originalAppUserId` or `originalTransactionId`) * `event_type`: Human-readable event name with `[Superwall]` prefix * `time`: Event timestamp (milliseconds) * `session_id`: Same as timestamp (groups related events) * `platform`: Store name (APP\_STORE, PLAY\_STORE, STRIPE) * `insert_id`: Unique event ID prefixed with `sw_` #### Revenue fields (when applicable) * `revenue`: Transaction amount (based on sales\_reporting setting) * `price`: Same as revenue * `quantity`: Always 1 * `productId`: Product identifier * `revenueType`: Same as event type (for revenue categorization) #### Event properties object All Superwall webhook data fields are included: * `id`, `name`, `cancelReason`, `exchangeRate` * `isSmallBusiness`, `periodType`, `countryCode` * `price`, `proceeds`, `priceInPurchasedCurrency` * `taxPercentage`, `commissionPercentage`, `takehomePercentage` * `offerCode`, `isFamilyShare`, `expirationAt` * `transactionId`, `originalTransactionId`, `originalAppUserId` * `store`, `purchasedAt`, `currencyCode`, `productId` * `environment`, `isTrialConversion`, `newProductId` * `bundleId`, `ts` ### Revenue tracking #### Automatic revenue attribution Revenue is automatically tracked for events with non-zero amounts: * **Positive revenue**: Purchases, renewals, conversions * **Negative revenue**: Refunds (automatically deducted) * **Zero revenue**: Cancellations, expirations, billing issues #### Revenue reporting options The `sales_reporting` setting determines which value is used: | Setting | Value Used | Description | | ------------ | ---------- | ------------------------------- | | `"Revenue"` | `price` | Gross revenue before store fees | | `"Proceeds"` | `proceeds` | Net revenue after store fees | #### Revenue examples **Initial Purchase ($9.99):** ```json { "event_type": "[Superwall] Subscription Start", "revenue": 9.99, "price": 9.99, "productId": "com.example.premium", "revenueType": "[Superwall] Subscription Start" } ``` **Refund (-$9.99):** ```json { "event_type": "[Superwall] Refund", "revenue": -9.99, "price": -9.99, "productId": "com.example.premium", "revenueType": "[Superwall] Refund" } ``` ### User identification The integration uses this hierarchy for user identification: 1. **Primary**: `originalAppUserId` (if available) 2. **Fallback**: `originalTransactionId` (always present) This ensures consistent user tracking across: * Multiple devices * App reinstalls * Legacy users without app user IDs #### Platform tracking The `platform` field identifies the payment source: * `APP_STORE`: iOS App Store * `PLAY_STORE`: Google Play Store * `STRIPE`: Stripe web payments This helps analyze: * Revenue by platform * Platform-specific retention * Cross-platform users ### Sandbox handling #### With sandbox API key If `sandbox_api_key` is configured: * Production events → Production project * Sandbox events → Sandbox project #### Without sandbox API key If `sandbox_api_key` is empty: * Production events → Production project * Sandbox events → **Skipped** (not sent) This prevents test data from polluting production analytics. ### Data residency Amplitude supports two data residency regions: | Region | API Endpoint | Use Case | | -------------- | -------------------- | --------------- | | `US (Default)` | api2.amplitude.com | Global, default | | `EU` | api.eu.amplitude.com | GDPR compliance | Choose based on: * Your data privacy requirements * User location * Compliance needs ### Session management Sessions are automatically managed: * `session_id` = Event timestamp * Groups rapid events together * New session for each subscription action * Helps track user journey ### Testing the integration #### 1. Trigger sandbox events * iOS: Use TestFlight with a sandbox Apple ID. StoreKit Configuration files do not generate App Store Server Notifications, so webhooks and downstream integrations won't fire. * Google Play: Use license test accounts to perform sandbox purchases. * Stripe: Use Stripe Test Mode to create sandbox transactions. #### 2. Verify in Amplitude Check your Amplitude project: 1. **User Lookup**: Find test user by ID 2. **Event Stream**: Verify events arriving 3. **Revenue Chart**: Confirm revenue tracking 4. **User Properties**: Check LTV calculation #### 3. Test different scenarios * Purchase event → Positive revenue * Refund event → Negative revenue * Cancellation → No revenue * Trial start → Event without revenue ### Best practices 1. **Consistent User IDs**: Send user IDs to app stores for better tracking 2. **Separate Environments**: Use sandbox API key for testing 3. **Revenue Model**: Choose gross vs net consistently 4. **Event Naming**: Use `[Superwall]` prefix to identify source 5. **Platform Analysis**: Segment by platform for insights 6. **Cohort Analysis**: Use trial conversion events for cohorts ### Common use cases #### Revenue analytics ``` Events: [Superwall] Subscription Start, [Superwall] Subscription Renewal Metric: Sum of revenue Segment by: platform, productId, countryCode ``` #### Conversion funnel ``` 1. [Superwall] Trial Start 2. [Superwall] Trial Conversion Conversion Rate: Step 2 / Step 1 ``` #### Churn analysis ``` Events: [Superwall] Subscription Cancellation Segment by: cancelReason, periodType, price tier ``` #### LTV calculation ``` Revenue Events: All [Superwall] events with revenue > 0 Group by: user_id Calculate: Sum of revenue per user ``` ### Troubleshooting #### Events not appearing 1. **Check API Key**: Verify key is correct for your project 2. **Check Region**: Ensure region matches your Amplitude project 3. **Check Environment**: Sandbox events need sandbox API key 4. **Check User ID**: Must have valid identifier #### Revenue not tracking 1. **Check Amount**: Only non-zero amounts create revenue 2. **Check Event Type**: Revenue fields only for purchase/renewal events 3. **Check Settings**: Verify Revenue vs Proceeds selection 4. **Check Refunds**: Negative amounts should decrease revenue #### Duplicate events The integration uses `insert_id` to prevent duplicates: * Format: `sw_eventId-eventName` * Amplitude automatically deduplicates by `insert_id` #### User attribution issues 1. **Check User ID**: Verify originalAppUserId is being sent 2. **Check Fallback**: originalTransactionId should always exist 3. **Platform Mismatch**: Ensure platform field is correct ### Rate limits Amplitude HTTP API v2 limits: * **Events per batch**: 1000 (we send 1 at a time) * **Request size**: 1MB (well within limit) * **Rate limit**: 1000 events/second per device * **Daily limit**: Based on your plan ### Integration with Amplitude features #### User properties While this integration sends events, consider: * Setting user properties separately * Using Identify API for user traits * Enriching profiles with app data #### Revenue verification Amplitude's revenue verification requires: * Receipt data (not included in webhooks) * Direct integration with app stores * This integration complements but doesn't replace revenue verification #### Predictive analytics Use Superwall events for: * Churn prediction models * LTV forecasting * Conversion probability scoring ### Data privacy * **User IDs**: Pseudonymous by default * **GDPR**: Use EU region for European users * **Data Retention**: Follows Amplitude project settings * **Deletion**: Handle via Amplitude's User Privacy API * **PII**: Avoid sending PII in event properties --- # Figma Plugin Source: https://superwall.com/docs/integrations/figma-plugin The Superwall Figma Plugin allows designers to convert Figma designs into fully functional paywalls with one click. The Superwall Figma Import plugin can automatically import Figma designs into the paywall editor. Each component is imported individually, preserving your design structure. Auto Layout is required for the **entire frame** in your Figma files for the import to work. To see it in action, check out the video demo: