WWDC Quick Look 💓 By SwiftGGTeam
What’s new in AdAttributionKit

What’s new in AdAttributionKit

Watch original video

Highlight

Starting with iOS 18.4, a single app can hold multiple active re-engagement conversion windows at once, with each ad recall tagged precisely by a conversion tag.


Core Content

Ad attribution has long sidestepped one problem: only one re-engagement conversion can run at any moment. Say you run two promo ads at the same time. The user taps Discount 1, opens the app, does not buy, then taps Discount 2 a few hours later, and ends up buying the Discount 1 product. Before iOS 18.3, the first conversion was already locked, and the purchase data could only attach to Discount 2. The first ad’s real value was buried. Marketers saw ROI numbers that did not match their gut, but had no place in attribution to fix it.

iOS 18.4 lifts this limit. Multiple re-engagement conversion windows can coexist, and each window carries a conversion tag — a kind of conversion bookmark. When the app sets EligibleForAdAttributionKitOverlappingConversions to YES in Info.plist, AdAttributionKit appends a query parameter to the re-engagement URL. The app reads the tag, persists it locally or on the server, and when the user actually buys, uses the tag to update the postback for that specific conversion. Attribution shifts from “who is closest to the purchase” to “which ad actually brought the user back.”


Details

The whole mechanism revolves around one query item: Postback.reengagementOpenURLParameter. After the app receives the Universal Link, it pulls the tag value out of the URL (05:42):

func retrieveConversionTag(fromURL url: URL) -> String? {
    guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
        print("Could not get components for URL.")
        return nil
    }

    guard let queryItems = components.queryItems else {
        print("URL does not contain query items.")
        return nil
    }

    for item in queryItems {
        guard item.name == Postback.reengagementOpenURLParameter else {
            continue
        }
        return item.value
    }
    return nil
}

Key points:

  • URLComponents(url:resolvingAgainstBaseURL:) parses the deep link; on failure it returns nil.
  • queryItems returns every query parameter on the URL; if there is none, treat it as a non-recall link.
  • Match against Postback.reengagementOpenURLParameter. AdAttributionKit always attaches this parameter on a re-engagement open.
  • Once found, return the value. That value is the conversion tag used for later updates.

After getting the tag, the business side calls the update API when the user takes a meaningful action (06:55):

func updateConversionValue(_ conversionValue: Int, conversionTag: String) async {
    do {
        let update = PostbackUpdate(fineConversionValue: conversionValue,
                                    lockPostback: false,
                                    conversionTag: conversionTag)
        try await Postback.updateConversionValue(update)
    }
    catch {
        print("An error occurred while updating the conversion value: \(error)")
    }
}

Key points:

  • PostbackUpdate takes three fields at once: fineConversionValue, lockPostback, and conversionTag.
  • lockPostback: false means further updates are still allowed, which fits multi-step funnels.
  • conversionTag is the key to a targeted update. Without it, the system only updates the most recent conversion.
  • Postback.updateConversionValue is async throws. The caller must handle errors in a concurrent context.

The second main thread is making the attribution window and cooldown configurable. Info.plist gains a new AdAttributionKitConfigurations field that declares window lengths per ad network (10:14):

{
  "AdAttributionKitConfigurations": {
    "AttributionWindows": {
      "com.example.adNetwork": {
        "install": {
          "click": 2,
          "ignoreInteractionType": "view"                     
        }
      }
    }
 }

Key points:

  • Under AttributionWindows, the ad network identifier is the key, so each network can be configured on its own.
  • The install child describes attribution for the install type. Re-engagement has no window config, since the conversion happens right next to the interaction.
  • click sets the click-attribution window in days. The default is 30 days; it can be shorter or longer.
  • ignoreInteractionType is set to "view" or "click", which excludes that interaction type from attribution for that network entirely.
  • For batch configuration, add a global key as a fallback; per-network configs override the global value (10:46).

Cooldown uses the same configuration entry to express a “lockout period after a conversion” (13:52). Setting install-cooldown-hours to 6 and reengagement-cooldown-hours to 1 prevents an install conversion from being snatched by a re-engagement just minutes later.

The postback also gains a country-code field this time around (17:05). The value is an ISO 3166 two-letter country code, such as "MT". In the marketplace flow, the install verification token payload picks up a matching ccode field (16:26), so ad networks can break down budget performance by region.


Key Takeaways

  • Turn on overlapping conversions and wire up the conversion tag: Why it matters: running several re-engagements at once is the norm during promo seasons, and the old rules systematically undervalue earlier ads. How to start: add EligibleForAdAttributionKitOverlappingConversions=YES to Info.plist, read Postback.reengagementOpenURLParameter in your Universal Link handler, and bind the tag to the local order context.

  • Tailor the attribution window per ad network: Why it matters: a default 30-day click window makes no sense for short promotions and pulls in unrelated traffic. A shorter window makes the data reflect real buying intent. How to start: use App Analytics to see each network’s actual conversion-time distribution, then set a 2–7 day click window in AttributionWindows for networks where typical conversion happens fast. For pure display networks, drop them entirely with ignoreInteractionType: "view".

  • Set a cooldown on critical conversion paths: Why it matters: a re-engagement intercepting an install only minutes after it completes is a real misattribution that pits your acquisition team against your recall team. How to start: look at the time gap between IAP and install, set install-cooldown-hours to the duration covering most first-time purchases (say, 6 hours), and reengagement-cooldown-hours to 1 hour. Then run the AdAttributionKit test entry under iOS Settings end to end.

  • Bring country-code into the postback pipeline: Why it matters: the same ROAS number means very different things in different countries. Splitting by region is the only way to find the markets that actually make money. How to start: add a country-code field to the postback ingest script. For the marketplace flow, also parse ccode out of the token payload as a dual record for compliance and reconciliation.


Comments

GitHub Issues · utterances