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.
This guide covers attribution for subscription apps. For gaming app attribution, see attribution for mobile gaming apps. For attribution models, see last-click vs multi-touch attribution.
The Subscription Attribution Funnel
Ad Click → Install → Onboarding → Trial Start → Trial Active → Trial-to-Paid → Renewal → Churn
Each step needs to be tracked and attributed back to the acquisition source:
| Event | Attribution Question |
|---|---|
| Install | Which campaign drove the install? |
| Trial start | Which channel's users start trials at the highest rate? |
| Trial-to-paid | Which source has the best trial conversion rate? |
| Renewal (Month 2+) | Which source retains subscribers longest? |
| Churn | Which source has the highest churn rate? |
| LTV (12 months) | Which channel delivers the highest lifetime value? |
Key Metrics by Acquisition Source
Funnel Metrics
| Metric | Definition | Why It Matters |
|---|---|---|
| Install-to-trial rate | Trial starts / installs | Onboarding effectiveness by source |
| Trial-to-paid rate | Paid conversions / trial starts | Quality of acquired users |
| Day 0 paid conversion | Users who paid immediately (no trial) | High-intent user percentage |
| Free-to-paid rate | Paid conversions / free installs | Overall conversion by source |
Revenue Metrics
| Metric | Definition | Why It Matters |
|---|---|---|
| ARPPU (Average Revenue Per Paying User) | Revenue / paying users | Revenue quality |
| LTV (Lifetime Value) | Total revenue per user over lifetime | True user value |
| LTV:CAC ratio | LTV / cost per acquisition | Campaign sustainability |
| Payback period | Months until CAC is recovered | Cash flow |
| ROAS (30/60/90/365 day) | Revenue / ad spend at each window | Time-boxed ROI |
Retention Metrics
| Metric | Definition | Why It Matters |
|---|---|---|
| Month 1 retention | % still subscribed after 1 month | Early churn indicator |
| Month 3 retention | % still subscribed after 3 months | Medium-term quality |
| Month 12 retention | % still subscribed after 12 months | Long-term value |
| Churn rate by source | Monthly churn rate per acquisition channel | Channel quality |
Tracking Subscription Events
Server-Side Event Tracking
Track subscription events on your server using App Store and Google Play server notifications:
// Apple App Store Server Notifications V2
app.post('/webhooks/apple', async (req, res) => {
const notification = decodeAppleNotification(req.body);
switch (notification.notificationType) {
case 'SUBSCRIBED':
await trackEvent(notification.originalTransactionId, 'trial_start', {
productId: notification.productId,
price: notification.price
});
break;
case 'DID_RENEW':
await trackEvent(notification.originalTransactionId, 'renewal', {
productId: notification.productId,
price: notification.price,
renewalNumber: notification.renewalCount
});
break;
case 'EXPIRED':
case 'DID_FAIL_TO_RENEW':
await trackEvent(notification.originalTransactionId, 'churn', {
reason: notification.expirationReason
});
break;
case 'DID_CHANGE_RENEWAL_STATUS':
if (!notification.autoRenewStatus) {
await trackEvent(notification.originalTransactionId, 'cancellation_intent', {});
}
break;
}
res.sendStatus(200);
});
// Google Play Real-Time Developer Notifications
app.post('/webhooks/google', async (req, res) => {
const notification = decodeGoogleNotification(req.body);
switch (notification.subscriptionNotification.notificationType) {
case 4: // SUBSCRIPTION_PURCHASED
await trackEvent(notification.purchaseToken, 'subscription_start', {
productId: notification.subscriptionId
});
break;
case 2: // SUBSCRIPTION_RENEWED
await trackEvent(notification.purchaseToken, 'renewal', {
productId: notification.subscriptionId
});
break;
case 13: // SUBSCRIPTION_EXPIRED
await trackEvent(notification.purchaseToken, 'churn', {});
break;
}
res.sendStatus(200);
});
Linking Subscriptions to Attribution
Connect subscription events to the original attribution source:
interface UserAttribution {
userId: string;
installSource: string; // 'facebook', 'google', 'organic'
campaignId?: string;
installDate: Date;
subscriptionEvents: SubscriptionEvent[];
}
async function getAttributedRevenue(
source: string,
dateRange: DateRange
): Promise<RevenueReport> {
const users = await db.query(`
SELECT
u.install_source,
u.campaign_id,
COUNT(DISTINCT u.user_id) AS users,
COUNT(DISTINCT CASE WHEN s.event = 'trial_start' THEN u.user_id END) AS trials,
COUNT(DISTINCT CASE WHEN s.event = 'subscription_start' THEN u.user_id END) AS paid,
SUM(CASE WHEN s.event IN ('subscription_start', 'renewal') THEN s.revenue ELSE 0 END) AS total_revenue
FROM users u
LEFT JOIN subscription_events s ON u.user_id = s.user_id
WHERE u.install_source = ?
AND u.install_date BETWEEN ? AND ?
GROUP BY u.install_source, u.campaign_id
`, [source, dateRange.start, dateRange.end]);
return users;
}
LTV Calculation by Source
Cohort-Based LTV
Calculate LTV by acquisition cohort and source:
async function calculateLTV(
source: string,
cohortMonth: string,
daysAfterInstall: number
): Promise<number> {
const result = await db.query(`
SELECT
AVG(cohort_revenue) AS avg_ltv
FROM (
SELECT
u.user_id,
SUM(CASE
WHEN s.event_date <= u.install_date + INTERVAL ? DAY
THEN s.revenue
ELSE 0
END) AS cohort_revenue
FROM users u
LEFT JOIN subscription_events s ON u.user_id = s.user_id
WHERE u.install_source = ?
AND DATE_FORMAT(u.install_date, '%Y-%m') = ?
GROUP BY u.user_id
) cohort
`, [daysAfterInstall, source, cohortMonth]);
return result[0].avg_ltv;
}
LTV Curves
Plot LTV over time for each acquisition source:
| Source | Day 0 LTV | Day 30 LTV | Day 90 LTV | Day 365 LTV |
|---|---|---|---|---|
| $0.00 | $4.50 | $12.00 | $38.00 | |
| Google Search | $0.00 | $5.20 | $14.50 | $45.00 |
| TikTok | $0.00 | $3.80 | $9.50 | $28.00 |
| Organic | $0.00 | $6.00 | $16.00 | $52.00 |
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.
Trial Optimization by Source
Trial Configuration Testing
Different acquisition sources may respond differently to trial configurations:
| Source | 7-Day Trial Conversion | 14-Day Trial Conversion | No Trial Conversion |
|---|---|---|---|
| Branded search | 45% | 42% | 30% |
| Social (Meta) | 25% | 30% | 12% |
| Display | 18% | 22% | 8% |
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.
SKAdNetwork and Subscription Events
On iOS, SKAdNetwork limits what you can report back to ad networks. For subscription apps, use the conversion value to encode subscription events:
// SKAN 4.0 conversion value scheme for subscription apps
func updateSKANConversion(event: SubscriptionEvent) {
var fineValue: Int = 0
switch event {
case .install:
fineValue = 0
case .trialStart:
fineValue = 10
case .trialDay3Active:
fineValue = 20
case .trialDay7Active:
fineValue = 30
case .paidConversion(let tier):
switch tier {
case .monthly: fineValue = 40
case .annual: fineValue = 50
case .premium: fineValue = 60
}
}
SKAdNetwork.updatePostbackConversionValue(fineValue, coarseValue: .high) { error in
if let error = error {
print("SKAN update failed: \(error)")
}
}
}
Tolinku for Subscription Attribution
Tolinku's analytics 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 Tolinku dashboard.
For mobile attribution, see mobile attribution: a developer's guide. For gaming app attribution, see attribution for mobile gaming apps.
Get deep linking tips in your inbox
One email per week. No spam.