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
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()returnsreadybefore you track conversions. - 2
Declare the postback endpoint (iOS)
Add two keys to your
Info.plistso iOS sends its attribution postbacks to appwin's endpoint:<!-- 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.iovalue delivers toappwin.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:
| Event | When to fire | Props |
|---|---|---|
app_install | Never - the SDK emits it | - |
start_trial | A free trial starts | - |
purchase | The store confirms a payment | { value, currency } |
subscribe | Optional: 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
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.
// 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()
}
// 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.
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:
| Network | In their console | In appwin |
|---|---|---|
| Meta | Events Manager → your dataset → Conversions API → generate a system-user token | Attribution → Integrations → Signal activation → Connect Meta: paste dataset ID + token |
| TikTok | Events Manager → copy TikTok App ID + access token | Add 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.