Subscription Event Technical Implementation Guide

Subscription Event Tracking Implementation Guide

Implement comprehensive subscription tracking to measure subscription revenue, user retention, and campaign performance across all platforms using Singular's SDK, server-to-server integration, or third-party partners.

Singular enables accurate attribution of subscription events—including new subscriptions, renewals, trials, cancellations, and refunds—to the marketing campaigns that generated users, providing complete visibility into subscription lifecycle and revenue generation.


Implementation Options

Choose the implementation method that best fits your app architecture and business requirements.

Consideration SDK Integration Server-to-Server (S2S) Third-Party Integration
Implementation Send subscription events via custom revenue methods in the Singular SDK Send events from your backend to Singular's REST API with required mobile device properties Configure integrations with RevenueCat, Adapty, or other subscription platforms
App Activity Dependency App must be launched for SDK to send events Events sent in real time, independent of app state Events sent in real time from partner platform
Event Timing Event time may differ from actual subscription time based on app launch Real-time event tracking as processed in backend Near real-time tracking after partner processing
Complexity Simple client-side implementation Requires secure backend infrastructure Requires partner account and configuration

Important: Most customers using S2S or third-party integrations for subscription events should still integrate the Singular SDK to manage sessions and non-subscription events for complete attribution tracking.


Android: Google Play Billing Integration

Integrate Google Play Billing Library 8.0.0 to query subscription information and manage subscription state directly on device for Singular SDK integration.

Prerequisites

Configure Google Play Console

Set up in-app products and subscription items in the Google Play Console before implementing billing in your app.

  1. Create Subscription Products: Define subscription tiers, billing periods, and pricing in Play Console
  2. Configure Offers: Set up base plans, offer tokens, and promotional pricing
  3. Test Setup: Create test accounts and verify product availability

Add Billing Library Dependency

Update build.gradle

Add Google Play Billing Library 8.0.0 to your app's dependencies with Kotlin extensions for coroutine support.

Kotlin DSL
dependencies {
    val billingVersion = "8.0.0"
    
    // Google Play Billing Library
    implementation("com.android.billingclient:billing:$billingVersion")
    
    // Kotlin extensions and coroutines support
    implementation("com.android.billingclient:billing-ktx:$billingVersion")
}

Version 8.0.0 Features: This version introduces automatic service reconnection with enableAutoServiceReconnection(), improved purchase query APIs, and enhanced pending transaction handling.


Initialize BillingClient

Create Billing Manager

Set up a billing manager class to handle all subscription operations and integrate with Singular SDK for event tracking.

Kotlin
class SubscriptionManager(private val context: Context) {
    
    private val purchasesUpdatedListener = PurchasesUpdatedListener { billingResult, purchases ->
        when (billingResult.responseCode) {
            BillingResponseCode.OK -> {
                purchases?.forEach { purchase ->
                    handlePurchaseUpdate(purchase)
                }
            }
            BillingResponseCode.USER_CANCELED -> {
                Log.d(TAG, "User canceled the purchase")
            }
            else -> {
                Log.e(TAG, "Purchase error: ${billingResult.debugMessage}")
            }
        }
    }
    
    private val billingClient = BillingClient.newBuilder(context)
        .setListener(purchasesUpdatedListener)
        .enablePendingPurchases() // Required for all purchases
        .enableAutoServiceReconnection() // NEW in 8.0.0 - handles reconnection automatically
        .build()
    
    fun initialize() {
        billingClient.startConnection(object : BillingClientStateListener {
            override fun onBillingSetupFinished(billingResult: BillingResult) {
                if (billingResult.responseCode == BillingResponseCode.OK) {
                    Log.d(TAG, "Billing client ready")
                    // Query existing subscriptions
                    queryExistingSubscriptions()
                } else {
                    Log.e(TAG, "Billing setup failed: ${billingResult.debugMessage}")
                }
            }
            
            override fun onBillingServiceDisconnected() {
                // With enableAutoServiceReconnection(), SDK handles reconnection
                // Manual retry is no longer required
                Log.d(TAG, "Billing service disconnected - auto-reconnection enabled")
            }
        })
    }
    
    companion object {
        private const val TAG = "SubscriptionManager"
    }
}

Key Features:

  • Auto-Reconnection: Version 8.0.0's enableAutoServiceReconnection() automatically re-establishes connections when needed
  • Purchase Listener: Receives callbacks for all purchase updates including renewals and pending transactions
  • Error Handling: Comprehensive handling for user cancellations and billing errors

Query Subscription Information

Retrieve Active Subscriptions

Query existing subscriptions using queryPurchasesAsync() to handle renewals and subscriptions purchased outside your app.

Kotlin
private suspend fun queryExistingSubscriptions() {
    val params = QueryPurchasesParams.newBuilder()
        .setProductType(BillingClient.ProductType.SUBS)
        .build()
    
    withContext(Dispatchers.IO) {
        val result = billingClient.queryPurchasesAsync(params)
        
        if (result.billingResult.responseCode == BillingResponseCode.OK) {
            result.purchasesList.forEach { purchase ->
                when (purchase.purchaseState) {
                    Purchase.PurchaseState.PURCHASED -> {
                        // Active subscription - process it
                        handleSubscriptionPurchase(purchase)
                    }
                    Purchase.PurchaseState.PENDING -> {
                        // Pending payment - notify user
                        Log.d(TAG, "Subscription pending: ${purchase.products}")
                    }
                }
            }
        } else {
            Log.e(TAG, "Query failed: ${result.billingResult.debugMessage}")
        }
    }
}

Best Practice: Call queryPurchasesAsync() in onResume() to catch subscription renewals that occurred while your app was backgrounded or closed.


Query Product Details

Retrieve subscription product details including pricing, billing periods, and offer tokens before displaying to users.

Kotlin
private suspend fun querySubscriptionProducts() {
    val productList = listOf(
        QueryProductDetailsParams.Product.newBuilder()
            .setProductId("premium_monthly")
            .setProductType(BillingClient.ProductType.SUBS)
            .build(),
        QueryProductDetailsParams.Product.newBuilder()
            .setProductId("premium_yearly")
            .setProductType(BillingClient.ProductType.SUBS)
            .build()
    )
    
    val params = QueryProductDetailsParams.newBuilder()
        .setProductList(productList)
        .build()
    
    withContext(Dispatchers.IO) {
        val productDetailsResult = billingClient.queryProductDetails(params)
        
        if (productDetailsResult.billingResult.responseCode == BillingResponseCode.OK) {
            // Process successfully retrieved product details
            productDetailsResult.productDetailsList?.forEach { productDetails ->
                // Extract subscription offers
                productDetails.subscriptionOfferDetails?.forEach { offer ->
                    val price = offer.pricingPhases.pricingPhaseList.firstOrNull()
                    Log.d(TAG, "Product: ${productDetails.productId}, Price: ${price?.formattedPrice}")
                }
            }
            
            // Handle unfetched products
            productDetailsResult.unfetchedProductList?.forEach { unfetchedProduct ->
                Log.w(TAG, "Unfetched: ${unfetchedProduct.productId}")
            }
        }
    }
}

Process Purchase Updates

Handle Subscription Events

Process purchase state changes and send appropriate events to Singular based on subscription lifecycle.

Kotlin
private fun handlePurchaseUpdate(purchase: Purchase) {
    when (purchase.purchaseState) {
        Purchase.PurchaseState.PURCHASED -> {
            if (!purchase.isAcknowledged) {
                // Verify purchase on your backend first
                verifyPurchaseOnBackend(purchase) { isValid ->
                    if (isValid) {
                        // Send subscription event to Singular
                        trackSubscriptionToSingular(purchase)
                        
                        // Grant entitlement to user
                        grantSubscriptionAccess(purchase)
                        
                        // Acknowledge purchase to Google
                        acknowledgePurchase(purchase)
                    }
                }
            }
        }
        Purchase.PurchaseState.PENDING -> {
            // Notify user that payment is pending
            Log.d(TAG, "Purchase pending approval: ${purchase.products}")
            notifyUserPendingPayment(purchase)
        }
    }
}

private fun trackSubscriptionToSingular(purchase: Purchase) {
    // Get cached product details for pricing information
    val productDetails = getCachedProductDetails(purchase.products.first())
    
    productDetails?.let { details ->
        val subscriptionOffer = details.subscriptionOfferDetails?.firstOrNull()
        val pricingPhase = subscriptionOffer?.pricingPhases?.pricingPhaseList?.firstOrNull()
        
        val price = pricingPhase?.priceAmountMicros?.div(1_000_000.0) ?: 0.0
        val currency = pricingPhase?.priceCurrencyCode ?: "USD"
        
        // Determine event name based on purchase type
        val eventName = if (isNewSubscription(purchase)) {
            "sng_subscribe"
        } else {
            "subscription_renewed"
        }
        
        // Send to Singular WITHOUT receipt
        Singular.customRevenue(
            eventName,
            currency,
            price,
            mapOf(
                "subscription_id" to purchase.products.first(),
                "order_id" to (purchase.orderId ?: ""),
                "purchase_time" to purchase.purchaseTime.toString()
            )
        )
        
        Log.d(TAG, "Tracked $eventName to Singular: $price $currency")
    }
}

private fun acknowledgePurchase(purchase: Purchase) {
    val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
        .setPurchaseToken(purchase.purchaseToken)
        .build()
    
    lifecycleScope.launch {
        val ackResult = withContext(Dispatchers.IO) {
            billingClient.acknowledgePurchase(acknowledgePurchaseParams)
        }
        
        if (ackResult.responseCode == BillingResponseCode.OK) {
            Log.d(TAG, "Purchase acknowledged successfully")
        } else {
            Log.e(TAG, "Acknowledgement failed: ${ackResult.debugMessage}")
        }
    }
}

Critical: Do NOT send the Google Play receipt to Singular when tracking subscription events. Use customRevenue() method without receipt validation. Acknowledge purchases within 3 days or they will be automatically refunded.


iOS: StoreKit Integration

Integrate Apple's StoreKit framework to manage subscriptions and send events to Singular for iOS apps.

Prerequisites

Configure App Store Connect

Set up subscription products and subscription groups in App Store Connect before implementing StoreKit in your app.

  1. Create Subscription Groups: Organize subscriptions into groups based on access levels
  2. Define Subscription Products: Set up subscription tiers, durations, and pricing
  3. Configure Offers: Create introductory offers, promotional offers, and subscription codes
  4. Test Environment: Set up sandbox test accounts for development

Implement StoreKit

Set Up Transaction Observer

Implement SKPaymentTransactionObserver to receive subscription purchase notifications and renewals.

Swift
import StoreKit

class SubscriptionManager: NSObject, SKPaymentTransactionObserver {
    
    static let shared = SubscriptionManager()
    
    private override init() {
        super.init()
        // Add transaction observer
        SKPaymentQueue.default().add(self)
    }
    
    deinit {
        SKPaymentQueue.default().remove(self)
    }
    
    // MARK: - SKPaymentTransactionObserver
    
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        for transaction in transactions {
            switch transaction.transactionState {
            case .purchased:
                // New subscription or renewal
                handlePurchasedTransaction(transaction)
                
            case .restored:
                // Subscription restored from another device
                handleRestoredTransaction(transaction)
                
            case .failed:
                // Purchase failed
                handleFailedTransaction(transaction)
                
            case .deferred:
                // Purchase awaiting approval (family sharing)
                print("Transaction deferred: \(transaction.payment.productIdentifier)")
                
            case .purchasing:
                // Transaction in progress
                break
                
            @unknown default:
                break
            }
        }
    }
    
    private func handlePurchasedTransaction(_ transaction: SKPaymentTransaction) {
        // Verify receipt with your backend
        verifyReceipt { isValid in
            if isValid {
                // Send to Singular
                self.trackSubscriptionToSingular(transaction)
                
                // Grant entitlement
                self.grantSubscriptionAccess(transaction)
                
                // Finish transaction
                SKPaymentQueue.default().finishTransaction(transaction)
            }
        }
    }
    
    private func trackSubscriptionToSingular(_ transaction: SKPaymentTransaction) {
        // Get product details for pricing
        let productId = transaction.payment.productIdentifier
        
        // You should cache product details from SKProductsRequest
        if let product = getCachedProduct(productId) {
            let price = product.price.doubleValue
            let currency = product.priceLocale.currencyCode ?? "USD"
            
            // Determine event name
            let eventName = isNewSubscription(transaction) ? "sng_subscribe" : "subscription_renewed"
            
            // Send to Singular WITHOUT receipt
            Singular.customRevenue(
                eventName,
                currency: currency,
                amount: price,
                withAttributes: [
                    "subscription_id": productId,
                    "transaction_id": transaction.transactionIdentifier ?? "",
                    "transaction_date": transaction.transactionDate?.timeIntervalSince1970 ?? 0
                ]
            )
        }
    }
}

Query Subscription Status

Use receipt validation to check current subscription status and handle renewals.

Swift
func refreshSubscriptionStatus() {
    let request = SKReceiptRefreshRequest()
    request.delegate = self
    request.start()
}

extension SubscriptionManager: SKRequestDelegate {
    func requestDidFinish(_ request: SKRequest) {
        // Receipt refreshed successfully
        if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
           FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {
            
            // Send receipt to your backend for validation
            validateReceiptOnServer()
        }
    }
    
    func request(_ request: SKRequest, didFailWithError error: Error) {
        print("Receipt refresh failed: \(error.localizedDescription)")
    }
}

SDK Integration Method

Send subscription events from your app using Singular SDK's custom revenue methods without receipt validation.

Platform-Specific SDK Methods

Use the appropriate SDK method for your platform to track subscription events.

Android SDK

Kotlin
// Track subscription without receipt
Singular.customRevenue(
    "sng_subscribe",  // Event name
    "USD",            // Currency
    9.99,             // Amount
    mapOf(            // Additional attributes
        "subscription_id" to "premium_monthly",
        "billing_period" to "monthly"
    )
)

Documentation: Android SDK Revenue Tracking


iOS SDK

Swift
// Track subscription without receipt
Singular.customRevenue(
    "sng_subscribe",  // Event name
    currency: "USD",  // Currency
    amount: 9.99,     // Amount
    withAttributes: [ // Additional attributes
        "subscription_id": "premium_monthly",
        "billing_period": "monthly"
    ]
)

Documentation: iOS SDK Revenue Tracking


React Native SDK

JavaScript
// Track subscription without receipt
Singular.customRevenueWithArgs(
    "sng_subscribe",  // Event name
    "USD",            // Currency
    9.99,             // Amount
    {                 // Additional attributes
        subscription_id: "premium_monthly",
        billing_period: "monthly"
    }
);

Documentation: React Native SDK Revenue Tracking


Unity SDK

C#
// Track subscription without receipt
SingularSDK.CustomRevenue(
    "sng_subscribe",  // Event name
    "USD",            // Currency
    9.99,             // Amount
    new Dictionary<string, object> { // Additional attributes
        { "subscription_id", "premium_monthly" },
        { "billing_period", "monthly" }
    }
);

Documentation: Unity SDK Revenue Tracking


Flutter SDK

Dart
// Track subscription without receipt
Singular.customRevenueWithAttributes(
    "sng_subscribe",  // Event name
    "USD",            // Currency
    9.99,             // Amount
    {                 // Additional attributes
        "subscription_id": "premium_monthly",
        "billing_period": "monthly"
    }
);

Documentation: Flutter SDK Revenue Tracking


Cordova SDK

JavaScript
// Track subscription without receipt
cordova.plugins.SingularCordovaSdk.customRevenueWithArgs(
    "sng_subscribe",  // Event name
    "USD",            // Currency
    9.99,             // Amount
    {                 // Additional attributes
        subscription_id: "premium_monthly",
        billing_period: "monthly"
    }
);

Documentation: Cordova SDK Revenue Tracking

Critical: DO NOT use IAP (In-App Purchase) methods or send receipt values to Singular for subscription tracking. Only use customRevenue() methods without receipts.


Server-to-Server Integration

Send subscription events in real time from your backend to Singular's REST API for immediate tracking independent of app state.

Implementation Requirements

Use Singular's Event Endpoint to send subscription events with revenue parameters from your secure backend.

Event Endpoint

Send POST requests to Singular's Event API with subscription event data and required mobile device properties.

Endpoint:

POST https://api.singular.net/api/v1/evt

Required Parameters:

  • SDK Key: Your Singular SDK key (a parameter)
  • Event Name: Subscription event name (n parameter)
  • Revenue: Subscription amount (amt parameter)
  • Currency: ISO 4217 currency code (cur parameter)
  • Device Identifiers: IDFA (iOS) or GAID (Android) for attribution
  • Platform: Operating system (p parameter)

Documentation: Event Endpoint Reference and Revenue Parameters


Example Request

cURL
curl -X POST 'https://api.singular.net/api/v1/evt' \
  -H 'Content-Type: application/json' \
  -d '{
    "a": "YOUR_SDK_KEY",
    "n": "sng_subscribe",
    "r": 9.99,
    "pcc": "USD",
    "idfa": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "p": "iOS",
    "install_time": 1697000000000,
    "extra": {
      "subscription_id": "premium_monthly",
      "order_id": "GPA.1234.5678.9012"
    }
  }'

SDK Not Required: If using S2S for subscription events, you don't need to send these events from the SDK. However, most apps should still integrate the SDK for session tracking and non-subscription events.


Third-Party Integrations

Configure subscription tracking through third-party platforms that manage subscription infrastructure and send events directly to Singular.

Supported Partners

Integrate with subscription management platforms that have built-in Singular support.

RevenueCat Integration

RevenueCat handles subscription infrastructure and automatically sends subscription events to Singular's REST API.

Setup Steps:

  1. Create RevenueCat Account: Set up your app in RevenueCat dashboard
  2. Configure Singular Integration: Add Singular SDK key in RevenueCat integrations settings
  3. Map Events: Configure which RevenueCat events should be sent to Singular
  4. Test Integration: Verify events appear in Singular dashboard

Documentation: RevenueCat Singular Integration


Adapty Integration

Adapty provides subscription analytics and sends events to Singular through their integration.

Setup Steps:

  1. Create Adapty Account: Register and configure your app in Adapty
  2. Enable Singular Integration: Navigate to integrations and add Singular credentials
  3. Configure Event Mapping: Select which subscription events to forward
  4. Verify Data Flow: Confirm events reach Singular successfully

Documentation: Adapty Singular Integration

Note: Third-party integrations are managed by partner platforms. Configuration must be completed in their respective dashboards. Singular SDK should still be integrated for session and non-subscription event tracking.


Subscription Event Types

Track various subscription lifecycle events to measure user engagement, retention, and revenue across the complete subscription journey.

Standard Event Names

Use Singular's standard event names for consistent reporting across platforms, or define custom event names based on your tracking requirements.

Subscription State Event Name SDK Method S2S Integration
New Subscription sng_subscribe customRevenue() with revenue amount (no receipt) Event endpoint with revenue parameters
Trial Start sng_start_trial event() method without revenue Standard event endpoint without revenue
Trial End sng_end_trial event() method without revenue Standard event endpoint without revenue
Subscription Renewal subscription_renewed customRevenue() with revenue amount (no receipt) Event endpoint with revenue parameters
Cancellation subscription_cancelled event() method without revenue Standard event endpoint without revenue
Refund subscription_refunded customRevenue() with negative amount (optional) OR event() without revenue Event endpoint with negative revenue (optional) OR standard event

Important: When sending subscription events via customRevenue(), only send events where isRestored is false. Restored purchases represent existing subscriptions, not new revenue.


Event Implementation Examples

New Subscription

Track when users purchase a new subscription for the first time.

Kotlin
// New subscription purchase
Singular.customRevenue(
    "sng_subscribe",
    "USD",
    9.99,
    mapOf(
        "subscription_tier" to "premium",
        "billing_period" to "monthly",
        "product_id" to "premium_monthly"
    )
)

Trial Start

Track when users begin a free trial period without revenue.

Kotlin
// Trial start (no revenue)
Singular.event(
    "sng_start_trial",
    mapOf(
        "trial_duration" to "7_days",
        "subscription_tier" to "premium"
    )
)

Subscription Renewal

Track automatic subscription renewals with revenue amount.

Kotlin
// Subscription renewal
Singular.customRevenue(
    "subscription_renewed",
    "USD",
    9.99,
    mapOf(
        "subscription_tier" to "premium",
        "renewal_count" to 3,
        "product_id" to "premium_monthly"
    )
)

Cancellation

Track when users cancel their subscription (no revenue event).

Kotlin
// Subscription cancelled (no revenue)
Singular.event(
    "subscription_cancelled",
    mapOf(
        "cancellation_reason" to "too_expensive",
        "days_active" to 45,
        "subscription_tier" to "premium"
    )
)

SKAdNetwork Measurement

Measure subscription events through Apple's SKAdNetwork for iOS privacy-compliant attribution.

SKAN Subscription Support

Singular can measure subscription event data via all integration methods (SDK, S2S, and third-party). Event measurement may be limited by SKAN postback timing windows depending on the SKAN version.

Hybrid SKAN Implementation

For apps using Singular SDK for lifecycle events and S2S/third-party integrations for subscriptions, enable Hybrid SKAN to combine both data sources.

Contact Support: Reach out to your Customer Success Manager (CSM) to enable Hybrid SKAN for your app. This ensures subscription events from backend sources are properly attributed in SKAN conversion values.

SKAN Postback Windows:

  • SKAN 4.0: Three postback windows (0-2 days, 3-7 days, 8-35 days) enable measurement of early subscription events and short-term renewals
  • SKAN 3.0: Single 24-hour postback window limits measurement to immediate subscriptions only
  • Conversion Values: Configure subscription events in your SKAN conversion value schema to prioritize high-value events

Best Practices and Validation

Follow implementation best practices and verify subscription tracking is working correctly before production deployment.

Implementation Best Practices

Key Guidelines

  • No Receipt Validation: Never send platform receipts (Google Play, App Store) to Singular when tracking subscriptions—use customRevenue() without receipts
  • Filter Restored Purchases: Only send events where isRestored is false to avoid duplicate revenue reporting
  • Backend Verification: Always verify purchases on your secure backend before sending events to Singular
  • Timely Acknowledgement: Acknowledge Google Play purchases within 3 days to prevent automatic refunds
  • Query on Resume: Call queryPurchasesAsync() in onResume() to catch renewals that occurred while app was closed
  • Consistent Event Names: Use standard Singular event names (sng_subscribe, subscription_renewed) for unified reporting
  • Include Metadata: Add attributes like subscription_tier, billing_period, and product_id for detailed analysis

Testing and Validation

Testing Checklist

  1. Test Environment Setup: Use sandbox/test accounts for both Google Play and App Store
  2. New Subscription: Verify sng_subscribe event sends correctly with proper amount and currency
  3. Trial Flow: Test trial start and trial end events without revenue
  4. Renewal Handling: Confirm renewals trigger subscription_renewed events with correct revenue
  5. Cancellation Tracking: Verify cancellation events fire when users cancel subscriptions
  6. Restore Filtering: Ensure restored purchases don't send duplicate events
  7. Singular Dashboard: Check events appear in Singular with correct attribution
  8. Cross-Device: Test subscription syncing across multiple devices

Common Issues

  • Missing Events: Verify billing client connection is active and queryPurchasesAsync() is called on app resume
  • Duplicate Events: Check that restored purchases are properly filtered with isRestored check
  • Wrong Revenue: Ensure price extraction from ProductDetails correctly divides micros by 1,000,000
  • Attribution Issues: Confirm device identifiers (IDFA/GAID) are properly included in S2S requests
  • Acknowledgement Failures: Verify backend receipt validation completes before acknowledgement

Support Resources: For troubleshooting assistance, contact Singular Support with SDK version, platform details, sample event payloads, and Singular dashboard screenshots showing the issue.