stubkit docs

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)
    }
}