What should you set up first for iOS ATT and SKAN?
A prompt flow that raises ATT opt-in and a conversion-value schema tied to your KPIs.
Related resources
A prompt flow that raises ATT opt-in and a conversion-value schema tied to your KPIs.
Related resources
Raise the ATT prompt authorization rate and design a SKAdNetwork 4.0 Conversion Value schema around your business KPIs. A 0.1pp difference in ATT opt-in translates directly into 5–10% of iOS CPI.
ATT (App Tracking Transparency) is the system prompt asking consent to use the IDFA; when declined, attribution downgrades to the single SKAdNetwork (SKAN) channel. SKAN 4.0 sends a Coarse (low/medium/high) + Fine (0–63) value at three windows (0–2 / 3–7 / 8–35 days). This guide covers (1) ATT prompt optimization, (2) SKAN CV schema design, (3) Adjust mapping setup.
Industry-average ATT authorization rates: games ~28%, commerce ~35%, fintech ~42% (as of 2025). Three levers to raise it:
import AppTrackingTransparency
import AdjustSdk
func showATTFlow(from vc: UIViewController) {
// 1. Check the value-perception moment (e.g. onboarding done)
guard OnboardingState.shared.isCompleted else { return }
// 2. Show the pre-prompt
let pre = PrePromptViewController(
title: "Consent for personalized recommendations",
body: "If you choose 'Allow' on the next screen, we can recommend content that better matches your interests.",
primaryAction: "Continue",
secondaryAction: "Skip for now"
)
pre.onPrimary = { requestSystemATT() }
pre.onSecondary = { /* queue retry after 7 days */ }
vc.present(pre, animated: true)
}
func requestSystemATT() {
ATTrackingManager.requestTrackingAuthorization { status in
// The Adjust SDK receives the result automatically
Analytics.track("att_response", [
"status": status.rawValue,
"is_authorized": status == .authorized
])
}
}
{
"NSUserTrackingUsageDescription":
"We use activity data to recommend content and offers that better match your interests."
}
SKAN 4.0 changed four things that matter to how you structure campaigns and read results.
low / medium / high — so low-traffic campaigns still collect signal.| Window | Value returned | Use it for |
|---|---|---|
| 0–2 days | Fine (0–63) or coarse | Activation quality — signup, tutorial, first purchase |
| 3–7 days | Coarse only (low/med/high) | Early retention or repeat behaviour |
| 8–35 days | Coarse only (low/med/high) | Long-run value banding — arrives last |
The second and third windows are three buckets, not sixty-four. Designing detailed LTV bands there collapses them into low/medium/high, so keep the granular design in window 1 and use the later windows for direction only.
On most channels finer campaign structure means better analysis. Under SKAN it means worse data: each slice carries less volume, drops to a lower crowd anonymity tier, and loses the fine value and the granular Source ID. On iOS, deliberately consolidating campaigns often buys more measurement than it costs in control.
Most disputes about SKAN numbers are really disputes about this step. SKAdNetwork does not split credit — Apple picks one ad per install and everyone else gets nothing. Knowing how that single winner is chosen explains most of the gaps you will argue about later.
A postback is not sent when the window closes. Apple adds a randomised delay on top, precisely so the arrival time cannot be used to identify a user. That means an install and its postback land in different weeks, and a campaign you paused on Monday keeps producing postbacks for days. Apple has changed the exact windows and delays between versions, so confirm the current values in Apple's SKAdNetwork documentation rather than trusting a number you read in a blog post — this one included.
AdAttributionKit now sits alongside SKAdNetwork, and network support differs. Before you rebuild a schema, confirm which framework each of your networks actually reads.
This is also why your MMP dashboard and your network dashboard disagree, sometimes badly. Neither is lying — they are counting different things on different clocks. Why SKAN and MMP numbers disagree walks through where each number comes from before you escalate it as a bug.
Crowd anonymity is Apple's guard against re-identification: when too few installs sit behind a postback, Apple returns less detail. It is the single most common reason a correctly implemented schema still comes back empty.
If fine values stopped arriving after a restructure, suspect the tier before you suspect the SDK. Consolidate campaigns to rebuild volume and re-check before changing any code.
Apple does not publish fixed tier thresholds and the rules change between versions — confirm against Apple's SKAdNetwork documentation before committing to a structure.
Map cumulative revenue + key-event occurrence combinations into 64 slots. Fine's 64 levels apply only in the day 0–2 window; days 3–7 and 8–35 get only Coarse's 3 levels.
| Fine CV | Condition | Coarse | Business meaning |
|---|---|---|---|
| 0 | install only | [object Object] | Install only |
| 1 | session_start ≥ 2 | [object Object] | Revisit |
| 5 | add_to_cart ×1 | [object Object] | Engagement start |
| 10 | add_to_cart ≥ 2 OR begin_checkout | medium | High engagement |
| 20 | purchase ×1 · revenue < $5 | medium | Low-value purchase |
| 30 | purchase ×1 · revenue $5–$20 | medium | Mid-value purchase |
| 45 | purchase ×1 · revenue $20–$50 | high | High-value purchase |
| 55 | purchase ≥ 2 OR revenue ≥ $50 | high | Loyal purchaser |
| 63 | subscribe (monthly) | high | Subscription conversion |
SKAN does not just reduce your data — it changes the unit of decision. Optimising an iOS campaign the way you optimise an Android one produces confident, wrong calls.
Spend lands on the day you paid for it. The matching postback lands days later, after the window closes and the random delay elapses. Divide today's spend by today's postbacks and you get a CPI for two different populations. Anchor every ratio to the install cohort, and treat the most recent days as incomplete until the window plus delay has fully passed.
If most of your postbacks carry a coarse value, a bidding strategy built on fine-value revenue tiers is optimising toward data that mostly is not there. Check the distribution first: a conversion value that is overwhelmingly 0 usually means the first window closes before your users reach the events you mapped, not that the users are worthless.
Aggregated, delayed, last-touch postbacks tell you what was attributed, never what was incremental. When the question is whether spend created demand, that needs a holdout or geo test — see measuring incrementality and the incrementality tool.
Two practical consequences: give changes a full window plus delay before judging them, and compare networks only on postbacks you received, never on a network's own modelled installs mixed in with them.
Enable SKAN 4.0class SKANDelegateHandler: NSObject, AdjustDelegate {
static let shared = SKANDelegateHandler()
func adjustConversionValueUpdated(_ conversionValue: NSNumber?) {
// Adjust computes the conversion value and calls this automatically
guard let cv = conversionValue?.intValue else { return }
Analytics.track("skan_cv_updated", [
"fine_value": cv,
"coarse_tier": cv < 10 ? "low" : cv < 45 ? "medium" : "high"
])
}
}
1) NSUserTrackingUsageDescription missing from Info.plist. 2) Device below iOS 14.5. 3) User already responded (reset via Settings → Privacy → Tracking, then retest). 4) Adjust SDK initialization must complete before the ATT call.
1) Confirm every network's SKAdNetwork identifier (.skadnetworkidentifier) is registered in Info.plist — usually 30–50 entries. 2) Adjust handles network redirects automatically, but each network must enable its SKAN channel separately. 3) Normal postback delay range: 0–24 hours.
The CV mapping is set too tight. Lower revenue thresholds, or expand mid-tier slots so events alone can raise CV. Campaigns below Apple's privacy threshold (~25 installs/day) get CV masked to NULL — merge small campaigns and run them together.