Skip to content
Back to Academia

SKAN and AdAttributionKit: measuring iPhone ads without the IDFA

11.06Mobile analytics and attribution
On this page

In September Meta brought the grocery delivery app 4,000 installs, 1,500 of them on iPhone, and the iPhone side cost 750,000 ₸ (the app is the same as in the lesson on MMPs in practice, the figures are made up). On 7 October the marketer opens AppsFlyer to count first orders from Meta on iPhone and sees three install counts: 300, 1,400 and 1,500. One report has almost no orders, and in another a fifth of the records are blank where a value should be. Nothing here is broken. If someone declined tracking, the MMP most likely can't match their install to the click, and the ad network learns about such installs from Apple itself: later, in aggregate, and with only as much detail as Apple chooses to give.

An envelope with code 0 and an envelope with no value are not equal: the code is decoded with the schema, and a blank field stays blank.
An envelope with code 0 and an envelope with no value are not equal: the code is decoded with the schema, and a blank field stays blank.

SKAdNetwork or AdAttributionKit

Apple runs attribution without tracking through two frameworks. SKAdNetwork (SKAN) works with the App Store. AdAttributionKit arrived in iOS 17.4 and also works with alternative app marketplaces, and on the SKAdNetwork page Apple says outright to use AdAttributionKit for app ad campaigns (SKAdNetwork, AdAttributionKit). Neither needs ATT permission.

They share the same conversion windows, data tiers and values, and they work together (how the two frameworks interoperate):

  • the system picks one winner across the impressions of both frameworks: a click beats a view, the most recent click wins, and no more than six impressions are considered;
  • when the app updates a value through SKAN, the system mirrors the call into AdAttributionKit;
  • an ad network ID works with both frameworks.

Their default attribution windows are the same too: 30 days from click to install and 24 hours from a view (Apple on ad attribution). In the example from the MMP lesson, Meta's click window in AppsFlyer is 7 days, so an install ten days after the click may well go to Meta at Apple while the MMP's regular attribution puts it into organic.

Which framework signs the ad is the network's decision, so that question goes to the networks and the MMP. AdAttributionKit has two differences that matter to a marketer:

  • Configurable windows. Since iOS 26 the app can change the windows in Info.plist: clicks from 1 to 30 days, views from 1 to 7, for all networks at once or for each separately. The same place sets a cooldown after an install or a re-engagement, during which no new conversions are credited to the app (attribution rules, iOS 26 release notes).
  • Re-engagement. Since iOS 18 AdAttributionKit credits an ad when a click reopens an app that is already installed: the system opens the app through a universal link and adds the AdAttributionKitReengagementOpen parameter to it (attributions and postbacks). The router from the lesson on deep links should let this parameter through rather than treat the link as unknown. Viewing an ad without a click doesn't produce a re-engagement, and Apple caps these attributions per app per month and per device per year.

What a postback contains

A postback is a message signed by Apple saying that a network's ad led to an install. It carries no IDFA and no user ID. Everything else depends on the postback data tier. Apple sets the tier by how many similar installs have built up for the publisher app, the advertised app, the country and the source identifier (postbacks in multiple conversion windows):

  • at tier 0 the first postback has only a two-digit source identifier, and there will be no second or third;
  • tier 1 adds a coarse value;
  • tier 2 has the fine value and a source identifier of up to four digits;
  • tier 3 also has the app where the ad was shown, and the country.

The fine value is a number from 0 to 63, and the app or the network decides what it means (conversion value). The coarse value is low, medium or high, and you set their meaning too (coarse values). The network uses the source identifier to carry campaign information, and two digits hold less of it than four.

That leads to a practical rule. The more installs one ad gets, the more detail its postbacks carry, so don't split an iPhone budget across dozens of ad sets: each gets few installs, and the postbacks drop to the lower tiers. The country is part of the tier calculation as well, so in a small market like Kazakhstan detail disappears sooner than where the same ad gets thousands of installs a day.

Zero and a blank field, as in the picture, are different things. In the delivery app's schema 0 means the app recorded the first launch and nothing else in the schema happened. A blank field means Apple didn't release a value at that tier, and what the person did in the app is unknown.

When to expect the data

Values are updated in three windows counted from the first launch. Once a window closes, the device sends the postback after a random delay: 24-48 hours for the first and 24-144 hours for the second and third (conversion windows in AdAttributionKit). AppsFlyer processes postbacks once a day: everything received in a day reaches the dashboard by 08:00 UTC the next day, so 8-32 hours later (AppsFlyer's SKAN guide).

WindowDays after first launchWhat the postback can carryApple's delay after the window endsIn AppsFlyer after first launch
First0-2Fine or coarse value24-48 hoursAfter 80-128 hours
Second3-7Coarse only24-144 hoursAfter 200-344 hours
Third8-35Coarse only24-144 hoursAfter 872-1,016 hours

A window can be closed early (lock window): the postback is then prepared at once and sent after the same delay, and updates for the rest of the window are ignored. The second and third postbacks only arrive if the ad was signed with SKAN 4 or AdAttributionKit and the app updated the value in that window. They belong to the same install and are not counted as installs.

For the September cohort, that means that for people who installed on 30 September the first window reaches AppsFlyer by 5 October and the second by the 14th. A report opened on 7 October already shows the whole first window for September but not the second: who from the end of the month ordered on days three to seven will show up later.

A value schema for the delivery app

On the phone, the AppsFlyer SDK sets values according to the schema in SKAN Conversion Studio: Settings → SKAN Conversion Studio, SKAN 4 mode (Conversion Studio). AppsFlyer's windows don't overlap: the second one measures days 3-7 only, not 0-7. So first decide which question each window answers. For the delivery app: did the person place a first order and for how much, did they order anything on days 3-7, and do they keep ordering?

Windowlowmediumhigh
First, days 0-2Opened the appsign_upfirst_purchase
Second, days 3-7Opened the appsign_upRevenue in the window above zero
Third, days 8-35Opened the appOne purchaseTwo or more purchase

In the second window high is set by revenue rather than by two events: when a coarse value has several conditions, AppsFlyer encodes it only once all of them have happened. Revenue above zero catches both a first order and a repeat one.

The fine value in the first window goes to first-order revenue and sign_up. Orders paid to the courier reach AppsFlyer from the server, as in the lesson on SDK integration, and Conversion Studio counts those events too. But the value on the phone only updates when the person opens the app again before the window ends. An order the courier closed, from someone who never opened the app again, won't make it into SKAN. A few AppsFlyer rules follow:

  • low is a session. In the first window this is mandatory, and in the second and third AppsFlyer recommends it: with no value update in a window there is no postback.
  • Revenue in dollars. AppsFlyer sets revenue ranges in USD, so convert the 9,000 ₸ average order at the current rate and build the scale so that it lands in the middle, not in an edge range. If you advertise on Meta, you need at least four ranges.
  • Closing a window early. You can switch it on by time or when high is reached. The first order then reaches the report sooner, but a second order in the same days no longer changes the value, and AppsFlyer estimates the install time less precisely.
  • Events in network postbacks. The schema's events must be mapped in the network's postbacks, and for Meta there is no SKAN attribution on iOS 14.5 and later without it (AppsFlyer's Meta FAQ).

If the values in the reports don't match the schema, AppsFlyer names the usual cause: another SDK in the app also updates conversion values (ATT and SKAN in AppsFlyer). If the Meta SDK went into the app for deferred deep links from Meta, ask the developer whether it updates these values too.

Write down the date of every schema change next to the report. Postbacks from installs encoded with the old schema keep arriving for several days, and AppsFlyer warns that SSOT data will be inaccurate for about 96 hours after a change. Don't change the schema in the middle of a test.

Three modes in AppsFlyer

In My Dashboards, metrics have an attribution method switch: Classic, SKAN and SSOT (My Dashboards). These are the three numbers from the start of the lesson:

ModeMeta installs on iPhoneWho it counts
Classic300Installs AppsFlyer linked to Meta under its regular rules, without SKAN
SKAN1,400All installs where Apple gave the win to Meta, including those already counted in Classic
SSOT1,500Classic and SKAN without repeats

You can't add 300 and 1,400: some people are in both reports. Single Source of Truth removes these repeats like this (SSOT guide). If AppsFlyer attributed an install itself, the SDK sets a bit inside the fine value, and in SKAN data it shows up as af_attribution_flag = true. So the flag only exists in postbacks with a fine value: of those 600 it is true for 150 and false for 450. The other 800 postbacks, with just a coarse value or nothing at all, carry no flag, and SSOT assigns them with a model. In September the model counted another 50 of them as repeats. That gives 300 + 450 + 750 = 1,500 installs and 200 repeats, 50 of which are model estimates.

SSOT is switched on in Conversion Studio, and the data appears 3-5 days later. If you pick SSOT in a report without switching it on in Conversion Studio, the numbers will be wrong. In SKAN mode AppsFlyer shows money in dollars even if the app's currency is tenge. AppsFlyer attributes Apple Ads on iPhone in the regular mode and without the IDFA, so the Apple Ads row from the MMP lesson reads as usual.

Reading first orders when some values are blank

The first postbacks for 1,400 Meta installs on iPhone look like this:

What the first postback carriesPostbacksOf these, with a first order
Fine value60048
Coarse value only50035 with high
Nothing300Unknown
Total1,400At least 83

There are 83 observed first orders in the first two days. That puts the cost per first order from Meta on iPhone at 750,000 ₸ / 83 = 9,036 ₸, and that is an upper bound. There may turn out to be more orders, but there can't be fewer.

A colleague suggests taking the share from the first row, 48 / 600 = 8%, applying it to all 1,400 and reporting 112 first orders. Don't. The data tier depends on how many installs a source has built up, so postbacks without a value come from smaller placements and campaigns, and they are not a random sample of all installs. Whether those 300 people resemble the rest, the table can't say. The SSOT model for them can go in the report, but as a separate row marked "estimate".

Comparing 9,036 ₸ with the D7 cost per first order from the MMP lesson is premature as well: this covers only the first two days. The line in the report could read: "Meta, iPhone, September: per SKAN, at least 83 first orders in the first two days, cost per first order no more than 9,036 ₸; the SSOT estimate for 300 postbacks without a value is shown separately."

Before you change the iPhone budget

Attribution, even without repeats, answers which ad gets credit for an order, not whether the order would have happened without the ad. The second question needs an incrementality test.

Exercise: a schema for your own app

Take a client's app or the delivery service from this lesson and put together one page.

The first thing to look at is the high column. If high in the first window is a sign-up or an add to cart, the SKAN report will count those, and you can't get a cost per first order out of it. Then look at low: if it isn't a session, some people won't produce a postback in the second and third windows at all.

The last check is the report line. If it has a single order count without "at least" or "estimate", observed postbacks and the model have been mixed in it, and the client will take the estimate for a fact. As in the delivery example: 83 orders are visible in the data, while 112 came from assuming the blank postbacks behave like the filled ones.

Video: SKAdNetwork basics

SKAdNetwork: Getting Started With The Basics

An introductory video from AppsFlyer, in English, to get used to the terms.

Need help applying this to your analytics? Discuss your project with Goodlabs →