{"id":1758,"date":"2026-07-15T17:00:00","date_gmt":"2026-07-15T22:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=1758"},"modified":"2026-03-07T03:50:08","modified_gmt":"2026-03-07T08:50:08","slug":"attribution-for-subscription-apps","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/attribution-for-subscription-apps\/","title":{"rendered":"Attribution for Subscription Apps: Revenue Tracking"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Subscription apps have a unique attribution challenge: the install is free, the value comes from ongoing payments. A user acquired from a Facebook ad who installs, starts a free trial, converts to paid, and stays subscribed for 18 months is worth far more than one who installs, starts a trial, and cancels. Attribution needs to track this entire lifecycle, not just the install.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This guide covers attribution for subscription apps. For gaming app attribution, see <a href=\"https:\/\/tolinku.com\/blog\/attribution-for-gaming-apps\/\">attribution for mobile gaming apps<\/a>. For attribution models, see <a href=\"https:\/\/tolinku.com\/blog\/last-click-vs-multi-touch-attribution\/\">last-click vs multi-touch attribution<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Subscription Attribution Funnel<\/h2>\n\n\n\n<pre><code>Ad Click \u2192 Install \u2192 Onboarding \u2192 Trial Start \u2192 Trial Active \u2192 Trial-to-Paid \u2192 Renewal \u2192 Churn\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Each step needs to be tracked and attributed back to the acquisition source:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead>\n<tr>\n<th>Event<\/th>\n<th>Attribution Question<\/th>\n<\/tr>\n<\/thead>\n<tbody><tr>\n<td>Install<\/td>\n<td>Which campaign drove the install?<\/td>\n<\/tr>\n<tr>\n<td>Trial start<\/td>\n<td>Which channel&#39;s users start trials at the highest rate?<\/td>\n<\/tr>\n<tr>\n<td>Trial-to-paid<\/td>\n<td>Which source has the best trial conversion rate?<\/td>\n<\/tr>\n<tr>\n<td>Renewal (Month 2+)<\/td>\n<td>Which source retains subscribers longest?<\/td>\n<\/tr>\n<tr>\n<td>Churn<\/td>\n<td>Which source has the highest churn rate?<\/td>\n<\/tr>\n<tr>\n<td>LTV (12 months)<\/td>\n<td>Which channel delivers the highest lifetime value?<\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Key Metrics by Acquisition Source<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Funnel Metrics<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead>\n<tr>\n<th>Metric<\/th>\n<th>Definition<\/th>\n<th>Why It Matters<\/th>\n<\/tr>\n<\/thead>\n<tbody><tr>\n<td>Install-to-trial rate<\/td>\n<td>Trial starts \/ installs<\/td>\n<td>Onboarding effectiveness by source<\/td>\n<\/tr>\n<tr>\n<td>Trial-to-paid rate<\/td>\n<td>Paid conversions \/ trial starts<\/td>\n<td>Quality of acquired users<\/td>\n<\/tr>\n<tr>\n<td>Day 0 paid conversion<\/td>\n<td>Users who paid immediately (no trial)<\/td>\n<td>High-intent user percentage<\/td>\n<\/tr>\n<tr>\n<td>Free-to-paid rate<\/td>\n<td>Paid conversions \/ free installs<\/td>\n<td>Overall conversion by source<\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Revenue Metrics<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead>\n<tr>\n<th>Metric<\/th>\n<th>Definition<\/th>\n<th>Why It Matters<\/th>\n<\/tr>\n<\/thead>\n<tbody><tr>\n<td>ARPPU (Average Revenue Per Paying User)<\/td>\n<td>Revenue \/ paying users<\/td>\n<td>Revenue quality<\/td>\n<\/tr>\n<tr>\n<td>LTV (Lifetime Value)<\/td>\n<td>Total revenue per user over lifetime<\/td>\n<td>True user value<\/td>\n<\/tr>\n<tr>\n<td>LTV:CAC ratio<\/td>\n<td>LTV \/ cost per acquisition<\/td>\n<td>Campaign sustainability<\/td>\n<\/tr>\n<tr>\n<td>Payback period<\/td>\n<td>Months until CAC is recovered<\/td>\n<td>Cash flow<\/td>\n<\/tr>\n<tr>\n<td>ROAS (30\/60\/90\/365 day)<\/td>\n<td>Revenue \/ ad spend at each window<\/td>\n<td>Time-boxed ROI<\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Retention Metrics<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead>\n<tr>\n<th>Metric<\/th>\n<th>Definition<\/th>\n<th>Why It Matters<\/th>\n<\/tr>\n<\/thead>\n<tbody><tr>\n<td>Month 1 retention<\/td>\n<td>% still subscribed after 1 month<\/td>\n<td>Early churn indicator<\/td>\n<\/tr>\n<tr>\n<td>Month 3 retention<\/td>\n<td>% still subscribed after 3 months<\/td>\n<td>Medium-term quality<\/td>\n<\/tr>\n<tr>\n<td>Month 12 retention<\/td>\n<td>% still subscribed after 12 months<\/td>\n<td>Long-term value<\/td>\n<\/tr>\n<tr>\n<td>Churn rate by source<\/td>\n<td>Monthly churn rate per acquisition channel<\/td>\n<td>Channel quality<\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Tracking Subscription Events<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Server-Side Event Tracking<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Track subscription events on your server using App Store and Google Play server notifications:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">\/\/ Apple App Store Server Notifications V2\napp.post(&#39;\/webhooks\/apple&#39;, async (req, res) =&gt; {\n  const notification = decodeAppleNotification(req.body);\n\n  switch (notification.notificationType) {\n    case &#39;SUBSCRIBED&#39;:\n      await trackEvent(notification.originalTransactionId, &#39;trial_start&#39;, {\n        productId: notification.productId,\n        price: notification.price\n      });\n      break;\n\n    case &#39;DID_RENEW&#39;:\n      await trackEvent(notification.originalTransactionId, &#39;renewal&#39;, {\n        productId: notification.productId,\n        price: notification.price,\n        renewalNumber: notification.renewalCount\n      });\n      break;\n\n    case &#39;EXPIRED&#39;:\n    case &#39;DID_FAIL_TO_RENEW&#39;:\n      await trackEvent(notification.originalTransactionId, &#39;churn&#39;, {\n        reason: notification.expirationReason\n      });\n      break;\n\n    case &#39;DID_CHANGE_RENEWAL_STATUS&#39;:\n      if (!notification.autoRenewStatus) {\n        await trackEvent(notification.originalTransactionId, &#39;cancellation_intent&#39;, {});\n      }\n      break;\n  }\n\n  res.sendStatus(200);\n});\n\n\/\/ Google Play Real-Time Developer Notifications\napp.post(&#39;\/webhooks\/google&#39;, async (req, res) =&gt; {\n  const notification = decodeGoogleNotification(req.body);\n\n  switch (notification.subscriptionNotification.notificationType) {\n    case 4: \/\/ SUBSCRIPTION_PURCHASED\n      await trackEvent(notification.purchaseToken, &#39;subscription_start&#39;, {\n        productId: notification.subscriptionId\n      });\n      break;\n\n    case 2: \/\/ SUBSCRIPTION_RENEWED\n      await trackEvent(notification.purchaseToken, &#39;renewal&#39;, {\n        productId: notification.subscriptionId\n      });\n      break;\n\n    case 13: \/\/ SUBSCRIPTION_EXPIRED\n      await trackEvent(notification.purchaseToken, &#39;churn&#39;, {});\n      break;\n  }\n\n  res.sendStatus(200);\n});\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Linking Subscriptions to Attribution<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Connect subscription events to the original attribution source:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">interface UserAttribution {\n  userId: string;\n  installSource: string;       \/\/ &#39;facebook&#39;, &#39;google&#39;, &#39;organic&#39;\n  campaignId?: string;\n  installDate: Date;\n  subscriptionEvents: SubscriptionEvent[];\n}\n\nasync function getAttributedRevenue(\n  source: string,\n  dateRange: DateRange\n): Promise&lt;RevenueReport&gt; {\n  const users = await db.query(`\n    SELECT\n      u.install_source,\n      u.campaign_id,\n      COUNT(DISTINCT u.user_id) AS users,\n      COUNT(DISTINCT CASE WHEN s.event = &#39;trial_start&#39; THEN u.user_id END) AS trials,\n      COUNT(DISTINCT CASE WHEN s.event = &#39;subscription_start&#39; THEN u.user_id END) AS paid,\n      SUM(CASE WHEN s.event IN (&#39;subscription_start&#39;, &#39;renewal&#39;) THEN s.revenue ELSE 0 END) AS total_revenue\n    FROM users u\n    LEFT JOIN subscription_events s ON u.user_id = s.user_id\n    WHERE u.install_source = ?\n      AND u.install_date BETWEEN ? AND ?\n    GROUP BY u.install_source, u.campaign_id\n  `, [source, dateRange.start, dateRange.end]);\n\n  return users;\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">LTV Calculation by Source<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Cohort-Based LTV<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Calculate LTV by acquisition cohort and source:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">async function calculateLTV(\n  source: string,\n  cohortMonth: string,\n  daysAfterInstall: number\n): Promise&lt;number&gt; {\n  const result = await db.query(`\n    SELECT\n      AVG(cohort_revenue) AS avg_ltv\n    FROM (\n      SELECT\n        u.user_id,\n        SUM(CASE\n          WHEN s.event_date &lt;= u.install_date + INTERVAL ? DAY\n          THEN s.revenue\n          ELSE 0\n        END) AS cohort_revenue\n      FROM users u\n      LEFT JOIN subscription_events s ON u.user_id = s.user_id\n      WHERE u.install_source = ?\n        AND DATE_FORMAT(u.install_date, &#39;%Y-%m&#39;) = ?\n      GROUP BY u.user_id\n    ) cohort\n  `, [daysAfterInstall, source, cohortMonth]);\n\n  return result[0].avg_ltv;\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">LTV Curves<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Plot LTV over time for each acquisition source:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead>\n<tr>\n<th>Source<\/th>\n<th>Day 0 LTV<\/th>\n<th>Day 30 LTV<\/th>\n<th>Day 90 LTV<\/th>\n<th>Day 365 LTV<\/th>\n<\/tr>\n<\/thead>\n<tbody><tr>\n<td>Facebook<\/td>\n<td>$0.00<\/td>\n<td>$4.50<\/td>\n<td>$12.00<\/td>\n<td>$38.00<\/td>\n<\/tr>\n<tr>\n<td>Google Search<\/td>\n<td>$0.00<\/td>\n<td>$5.20<\/td>\n<td>$14.50<\/td>\n<td>$45.00<\/td>\n<\/tr>\n<tr>\n<td>TikTok<\/td>\n<td>$0.00<\/td>\n<td>$3.80<\/td>\n<td>$9.50<\/td>\n<td>$28.00<\/td>\n<\/tr>\n<tr>\n<td>Organic<\/td>\n<td>$0.00<\/td>\n<td>$6.00<\/td>\n<td>$16.00<\/td>\n<td>$52.00<\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Google Search users have the highest paid LTV (organic is higher but has no acquisition cost). TikTok users have the lowest LTV, suggesting lower-quality traffic for this app type.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Trial Optimization by Source<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Trial Configuration Testing<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Different acquisition sources may respond differently to trial configurations:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead>\n<tr>\n<th>Source<\/th>\n<th>7-Day Trial Conversion<\/th>\n<th>14-Day Trial Conversion<\/th>\n<th>No Trial Conversion<\/th>\n<\/tr>\n<\/thead>\n<tbody><tr>\n<td>Branded search<\/td>\n<td>45%<\/td>\n<td>42%<\/td>\n<td>30%<\/td>\n<\/tr>\n<tr>\n<td>Social (Meta)<\/td>\n<td>25%<\/td>\n<td>30%<\/td>\n<td>12%<\/td>\n<\/tr>\n<tr>\n<td>Display<\/td>\n<td>18%<\/td>\n<td>22%<\/td>\n<td>8%<\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Social users benefit more from longer trials (they need more time to experience the product), while search users already have intent and convert well with shorter trials or no trial at all.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">SKAdNetwork and Subscription Events<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">On iOS, <a href=\"https:\/\/developer.apple.com\/documentation\/storekit\/skadnetwork\/\" rel=\"nofollow noopener\" target=\"_blank\">SKAdNetwork<\/a> limits what you can report back to ad networks. For subscription apps, use the conversion value to encode subscription events:<\/p>\n\n\n\n<pre><code class=\"language-swift\">\/\/ SKAN 4.0 conversion value scheme for subscription apps\nfunc updateSKANConversion(event: SubscriptionEvent) {\n    var fineValue: Int = 0\n\n    switch event {\n    case .install:\n        fineValue = 0\n    case .trialStart:\n        fineValue = 10\n    case .trialDay3Active:\n        fineValue = 20\n    case .trialDay7Active:\n        fineValue = 30\n    case .paidConversion(let tier):\n        switch tier {\n        case .monthly: fineValue = 40\n        case .annual: fineValue = 50\n        case .premium: fineValue = 60\n        }\n    }\n\n    SKAdNetwork.updatePostbackConversionValue(fineValue, coarseValue: .high) { error in\n        if let error = error {\n            print(&quot;SKAN update failed: \\(error)&quot;)\n        }\n    }\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Tolinku for Subscription Attribution<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/tolinku.com\/features\/analytics\">Tolinku&#39;s analytics<\/a> track deep link clicks and attribute conversions, including subscription events. When a user clicks a Tolinku deep link and later subscribes, the conversion is attributed to the original click source. Configure subscription event tracking in the <a href=\"https:\/\/tolinku.com\/docs\/user-guide\/analytics\/\">Tolinku dashboard<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For mobile attribution, see <a href=\"https:\/\/tolinku.com\/blog\/mobile-attribution-developers-guide\/\">mobile attribution: a developer&#39;s guide<\/a>. For gaming app attribution, see <a href=\"https:\/\/tolinku.com\/blog\/attribution-for-gaming-apps\/\">attribution for mobile gaming apps<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Track attribution for subscription-based apps. Measure trial-to-paid conversions, LTV by channel, and subscription revenue by source.<\/p>\n","protected":false},"author":2,"featured_media":1757,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Attribution for Subscription Apps: Revenue Tracking","rank_math_description":"Track attribution for subscription-based apps. Measure trial-to-paid conversions, LTV by channel, and subscription revenue by source.","rank_math_focus_keyword":"attribution subscription apps","rank_math_canonical_url":"","rank_math_facebook_title":"","rank_math_facebook_description":"","rank_math_facebook_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-attribution-for-subscription-apps.png","rank_math_facebook_image_id":"","rank_math_twitter_title":"","rank_math_twitter_description":"","rank_math_twitter_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-attribution-for-subscription-apps.png","footnotes":""},"categories":[14],"tags":[37,28,520,20,69,530,146,231],"class_list":["post-1758","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-analytics","tag-analytics","tag-attribution","tag-campaign-measurement","tag-deep-linking","tag-mobile-development","tag-revenue","tag-saas","tag-subscriptions"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1758","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/comments?post=1758"}],"version-history":[{"count":3,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1758\/revisions"}],"predecessor-version":[{"id":2700,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1758\/revisions\/2700"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/1757"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=1758"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=1758"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=1758"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}