Attribution
stubkit can record campaign context (AppsFlyer / Adjust / Branch / UTM) alongside each user so you can see LTV by campaign. Call the attribution endpoint once per user — ideally at first launch when the deeplink or attribution SDK resolves a campaign.
Record a touch
POST /v1/attribution
Authorization: Bearer pk_live_...
{
"app_id": "notesam",
"user_id": "c6e42d...",
"campaign_id": "c_12345",
"source": "appsflyer",
"medium": "cpc",
"campaign_name": "Spring upgrade",
"ad_id": "creative_789",
"metadata": { "placement": "story" }
}All fields except app_id and user_id are optional. Multiple touches are allowed — first-touch wins in aggregation.
LTV by campaign
The dashboard queries GET /v1/admin/analytics/ltv-by-campaign which joins first-touch attribution with subscriptions and sums lifetime revenue per campaign. Users without any recorded touch fall into the organic bucket.
GET /v1/admin/analytics/ltv-by-campaign?app_id=notesam
{
"breakdown": [
{
"campaign": "Spring upgrade",
"source": "appsflyer",
"user_count": 420, "subscription_count": 435,
"total_revenue_cents": 1248000,
"avg_ltv_cents": 2971
}
]
}iOS — AppsFlyer example
AppsFlyerLib.shared().onConversionDataSuccess = { data in
guard let campaignId = data["campaign"] as? String else { return }
Task {
var req = URLRequest(url: URL(string: "https://api.stubkit.com/v1/attribution")!)
req.httpMethod = "POST"
req.setValue("Bearer pk_live_...", forHTTPHeaderField: "Authorization")
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpBody = try? JSONSerialization.data(withJSONObject: [
"app_id": "notesam",
"user_id": userId,
"campaign_id": campaignId,
"source": "appsflyer",
"metadata": data
])
_ = try? await URLSession.shared.data(for: req)
}
}