Attribution

Attribution shows what your ad budget buys: spend and ROAS next to your revenue, Apple's SKAdNetwork postbacks on your own endpoint, and your conversion events sent back to the networks.

The module builds on Appwin Core and Analytics: conversions travel through SDK events, everything else is configured in the dashboard. Its three parts - connected ad accounts, postback endpoint, conversion events - are independent: each works without the others.

Installation

  1. 1

    Install the SDK and initialize Analytics

    Attribution does not install on its own: it reuses Analytics events. The Quickstart generates the Appwin Core and Analytics install for every platform. Make sure Analytics initialize() returns ready before you track conversions.

  2. 2

    Declare the postback endpoint (iOS)

    Add two keys to your Info.plist so iOS sends its attribution postbacks to appwin's endpoint:

    xml
    <!-- SKAdNetwork (all supported iOS versions) -->
    <key>NSAdvertisingAttributionReportEndpoint</key>
    <string>https://appwin.io</string>
    
    <!-- AdAttributionKit (iOS 17.4+) -->
    <key>AttributionCopyEndpoint</key>
    <string>https://appwin.io</string>
    

    You get the SKAN observed column: installs Apple itself attributed to your ads. These are build-time keys read by iOS; no SDK can set them at runtime. Postbacks arrive 24-144h after the install (never compare them day-to-day with the networks' numbers), and iOS normalizes the endpoint to the registrable domain: any *.appwin.io value delivers to appwin.io.

Connect your ad accounts

Attribution → Integrations → Connect (TikTok, Meta). OAuth, read-only, no code: appwin never writes to an ad account. You get daily spend per network, campaign, ad group, and ad with creative thumbnails, the cross-network campaigns table, and ROAS crossed with your revenue.

These numbers are Reported by network (what the platforms claim): appwin never silently mixes them with what it observes itself.

Track conversions

Prerequisite: SDK installed, Analytics initialized. The whole step runs on four event names:

EventWhen to fireProps
app_installNever - the SDK emits it-
start_trialA free trial starts-
purchaseThe store confirms a payment{ value, currency }
subscribeOptional: a subscription activates (vs one-shot sale){ value, currency }

Fire them where your purchase pipeline confirms the transaction, after the store says yes, never on restores. On purchase, the amount travels as value (number) and currency (ISO 4217); any other prop stays home.

With RevenueCat

swift
let result = try await Purchases.shared.purchase(package: package)
guard !result.userCancelled else { return }

let product = package.storeProduct
let isTrial = result.customerInfo.entitlements.active.values
  .contains { $0.periodType == .trial }

if isTrial {
  AppwinAnalytics.track("start_trial")
} else {
  AppwinAnalytics.track("purchase", props: [
    "value": Double(truncating: product.price as NSNumber),
    "currency": product.currencyCode ?? "USD",
  ])
}

Your RevenueCat webhook (Revenue product) and these calls are complementary: the webhook feeds revenue analytics server-side, track() runs on the device - which is what conversion values and activation need.

With another purchase stack

Same pattern: one track in the success callback.

swift
// StoreKit 2, no purchase SDK
let result = try await product.purchase()
if case .success(let verification) = result,
   case .verified(let transaction) = verification {
  AppwinAnalytics.track("purchase", props: [
    "value": Double(truncating: product.price as NSNumber),
    "currency": product.priceFormatStyle.currencyCode,
  ])
  await transaction.finish()
}
swift
// Adapty
let result = try await Adapty.makePurchase(product: product)
if !result.isPurchaseCancelled {
  AppwinAnalytics.track("purchase", props: [
    "value": Double(truncating: product.price as NSNumber),
    "currency": product.currencyCode ?? "USD",
  ])
}

Qonversion, Glassfy, direct Play Billing: identical idea.

With nothing else to code, you get:

  • Conversion values (iOS) - appwin manages the SKAdNetwork conversion value from your events. Schema in the dashboard; defaults: trial = 32, purchase = 63.
  • Install sources (Android) - the Play Install Referrer of each install, in the overview.
  • Network activation - your events forwarded to Meta (server-side) and TikTok (embedded adapter), consent-gated.

Turn on network activation

Consent first: activation is opt-in. Relay the user's choice with one call, typically wired to the ATT prompt on iOS.

swift
let authorized = await AppwinCore.requestTrackingAuthorization()
AppwinCore.setAdvertisingConsent(authorized ? .granted : .denied)

Declare NSUserTrackingUsageDescription in your Info.plist. Already running a CMP? Keep it, just relay its advertising verdict.

Consent decides whether events flow to the networks; ATT only decides whether the advertising identifier enriches them. A refused ATT still activates, at lower match quality.

Then wire each network:

NetworkIn their consoleIn appwin
MetaEvents Manager → your dataset → Conversions API → generate a system-user tokenAttribution → Integrations → Signal activation → Connect Meta: paste dataset ID + token
TikTokEvents Manager → copy TikTok App ID + access tokenAdd the AppwinTikTokEvents package (iOS) / io.appwin:appwin-tiktok-events (Android) - its presence is the integration - then paste both in Connect TikTok

Finally, copy your conversion value schema (dashboard, Attribution → SKAN) into each network's console so they decode postbacks the same way. With the default schema that is two rows: 32 = trial started (coarse: medium), 63 = purchase (coarse: high).

Rules and limits

  • Per-install source on iOS exists for nobody: SKAN answers in aggregate, and that is the point. Appwin is not a certified MMP - it builds on the open rails every developer is entitled to.
  • The Play referrer says what the advertiser put in the store link; untagged campaigns show as "other".
  • Match quality depends on what users consent to share. The dashboard always separates Reported by network from Observed by appwin.

Next