SKAN 4.0
AppsFlyer · Advanced Attribution Intelligence

iOS Privacy-First
Measurement Playbook

ICM (Integrated Conversion Management), ODM (On Device Measurement), Fine & Coarse Values — plus full media partner integration intelligence and Flutter implementation guide.

🔒 Privacy-First 📊 SKAN 4.0 🎯 AppsFlyer SDK 💻 Flutter / Dart 📡 ODM 🔗 ICM

What is SKAdNetwork?

Apple's privacy-preserving attribution framework — aggregate campaign measurement without device-level data or IDFA dependency.

64Fine conversion values (0–63)
3Coarse values: Low / Mid / High
3Conversion windows (SKAN 4.0)
35dMaximum measurement window

Why SKAN Exists

  • iOS 14.5+ App Tracking Transparency (ATT) framework
  • User privacy — no device-level data sharing
  • Aggregate campaign performance measurement only
  • Eliminates IDFA dependency for attribution

SKAN 4.0 Key Additions

  • Three conversion windows (vs one in SKAN 3)
  • Coarse conversion value as privacy fallback
  • Source domain for web-to-app attribution
  • Crowd anonymity for higher privacy thresholds

SKAN Data Flow

1
Ad Display

User sees ad in source app. Ad network registers impression with Apple.

2
App Install

User installs advertised app from App Store. SKAN install notification generated.

3
Conversion Value Updates

App calls updatePostbackConversionValue — fine (0–63) and coarse (low/mid/high) updated within window.

4
Timer Window + Random Delay

Apple applies crowd anonymity threshold and random delay before dispatching postback.

5
Postback to Ad Network

Apple sends SKAN postback to registered ad network endpoint. AppsFlyer receives copy via configured postback URL.

Digital App Signals in SKAN

PRIMARY SIGNALS

📱 Conversion Value (CV)

Range 0–63 (6-bit). Encodes post-install behaviour. Can only increase, never decrease.

🎯 Source App ID

Bundle ID of app where ad was shown. Only shared if privacy threshold met.

🏷️ Campaign ID

Range 1–100. Identifies which campaign drove the install.

SKAN 4.0 ADDITIONS

📈 Coarse Conversion Value

Low / Medium / High — privacy fallback available across all 3 windows.

🔗 Source Domain

Web-to-app attribution via Safari/web. Shows domain where ad was displayed.

⏰ Multiple Windows

W1: 0–2d (fine+coarse) · W2: 3–7d (coarse) · W3: 8–35d (coarse)

AppsFlyer SKAN Solution

AppsFlyer layers intelligent CV management, predictive analytics, and a unified dashboard on top of Apple's raw SKAN framework.

1
SDK Integration

AppsFlyer SDK intercepts in-app events and manages CV update calls automatically.

2
Event → CV Mapping

Conversion Studio schema maps events to 6-bit CV values with configurable priority.

3
Smart CV Management

Optimised monotone-increasing updates within Apple's constraints across all three windows.

4
Postback Ingestion

SKAN postbacks received, parsed, and enriched with modelled data where CV is null.

5
Analytics + Reporting

Single-source-of-truth dashboard: SKAN, modelled, and probabilistic data unified.

❌ What SKAN Does NOT Send

  • User ID or Device ID (IDFA)
  • Exact install or conversion timestamp
  • IP address or geolocation
  • User demographics
  • Exact revenue amounts

✅ What SKAN DOES Send

  • Fine conversion value (0–63) — Window 1
  • Coarse value (low/mid/high) — all windows
  • Campaign ID (1–100)
  • Source app ID (above privacy threshold)
  • Source domain (web-to-app, SKAN 4.0)

AppsFlyer SKAN Advantages

PredictiveLTV Modelling

ML-estimated lifetime value from early behaviour signals

UnifiedSingle Dashboard

SKAN + probabilistic + deterministic data in one view

SmartCV Optimisation

AI-powered Conversion Studio schema recommendations

ICM — Integrated Conversion Management

AppsFlyer's statistical methodology that fills attribution gaps from ATT opt-out traffic — using aggregated signals, probabilistic matching, and ML to model conversions that SKAN alone cannot capture.

What Is ICM and Why Does It Exist?

With iOS 14.5+, a large proportion of iOS traffic operates with no IDFA and limited SKAN signal. Integrated Conversion Management estimates attribution for this "dark traffic" using available aggregated signals — without compromising Apple's privacy framework.

~40%iOS traffic is "unknown" post-ATT
More attribution coverage vs SKAN-only
0User-level data used (fully aggregate)

How ICM Works — 5-Step Process

📲
Install Detected
No IDFA. Device signals captured at install time.
🔬
Signal Capture
IP, device model, OS, carrier, timezone, language.
🧠
ML Matching
Probabilistic engine compares vs known cohort patterns.
📐
Credit Assigned
Modelled attribution assigned to campaign/source.
📊
Dashboard
Modelled conversions surfaced alongside SKAN data.

Integration by Media Partner

Google Ads
App Campaigns · UAC · PMax
Mature

Deep SKAN integration via Google's own SKAd postback ingestion. ICM supplements Google's proprietary NCS (Network Conversion Signals) modelling to fill gaps in opt-out traffic.

DirectionData Shared
AF → GoogleSKAN campaign IDs, CV values (fine + coarse), conversion type labels, bid signals
Google → AFModelled install counts, campaign performance aggregates, tROAS/tCPA signals
Apple → GoogleDirect SKAN postback (Apple-registered endpoint)
Direct SKAN Postback tROAS Optimisation NCS Modelling
Meta (Facebook/Instagram)
AEM · CAPI · Advantage+
Complex

Uses Aggregated Event Measurement (AEM) alongside SKAN. Meta's Conversions API (CAPI) allows server-side event sharing to supplement SKAN's limited signal.

DirectionData Shared
AF → MetaUp to 8 prioritised events (AEM), aggregated conversion values, SKAN postbacks
Meta → AFModelled conversions (VTA + CTA), campaign cost data
Constraint8-event max (AEM), 7-day click / 1-day view attribution window
AEM (8 Events) 88% Null CV Risk CAPI Integration
Apple Search Ads
Native · Search · Browse · Today
Best Quality

Highest-fidelity SKAN integration due to Apple's native relationship. ASA receives direct SKAN postbacks from Apple, with AppsFlyer reconciling data for unified reporting.

DirectionData Shared
Apple → ASADirect SKAN postback (campaign ID, ad group ID, keyword ID, creative set)
ASA → AFGranular campaign hierarchy, Search Match vs Exact, Creative Set performance
Null CV Rate~0.01% — industry lowest via Apple native integration
0.01% Null CV Keyword Granularity Native Crowd Anonymity
Amazon Ads
DSP · Sponsored · Attribution
Maturing

Amazon Attribution tags supplement SKAN postbacks. DSP operates via separate tracking. ICM fills gaps where Amazon's direct SKAN integration is less mature.

DirectionData Shared
AF → AmazonSKAN campaign IDs, modelled install counts, aggregated conversion events
Amazon → AFCampaign IDs, creative IDs, ASIN-level performance, DSP audience segments
ICM RoleProbabilistic modelling fills gaps in Amazon's SKAN coverage
Attribution Tags DSP Separate Flow ASIN Segmentation
TikTok
In-Feed · TopView · Spark Ads
Strong

TikTok Events API complements SKAN postbacks. TikTok's own VTA modelling is reconciled with AppsFlyer ICM for unified cross-channel reporting.

DirectionData Shared
AF → TikTokSKAN campaign IDs (1–100), aggregated event types, coarse and fine CV, modelled attribution
TikTok → AFVTA modelled installs, campaign cost data, creative performance, audience insights
Null CV Rate~5.67% — good campaign consolidation
5.67% Null CV Events API VTA Modelling
Reddit
Promoted Posts · Takeover · CAPI
Emerging

Newer SKAN integration — Reddit primarily serves upper-funnel awareness campaigns. CAPI integration is less mature; ICM plays a significant role in filling measurement gaps.

DirectionData Shared
AF → RedditSKAN postback data, event categories (install, engage, purchase), campaign IDs
Reddit → AFCampaign IDs, ad group IDs, community targeting, cost data
ICM RoleHigh ICM reliance — Reddit's SKAN integration still maturing
CAPI (Maturing) Upper Funnel Focus High ICM Reliance

Partner Data Quality Comparison

PartnerSKAN MaturityAvg Null CV RateICM RelianceDirect Postback
Apple Search Ads⭐⭐⭐⭐⭐ Native~0.01%Very Low✅ Yes (Apple direct)
TikTok⭐⭐⭐⭐ Strong~5.67%Low–Medium✅ Yes
Google Ads⭐⭐⭐⭐ Strong~30%Medium✅ Yes
Meta⭐⭐⭐ Good~88%High✅ Yes (via AEM)
Amazon⭐⭐⭐ Maturing~35%Medium–High⚠️ Partial
Rakuten⭐⭐ Affiliate~40%High⚠️ Click-based
Reddit⭐⭐ Emerging~50%Very High⚠️ Partial

ODM — On Device Measurement

AppsFlyer's proprietary attribution methodology that infers the complete attribution picture from a single device's available signals — without cross-device tracking, IDFA, or user-level data.

What Is ODM?

ODM is designed for the post-ATT world where IDFA is unavailable. Rather than linking multiple devices or users, it focuses entirely on what can be inferred from a single device's interaction at the moment of install.

  • Fully device-level (no cross-user linking)
  • Privacy-safe — no PII required
  • Probabilistic, not deterministic
  • Complements SKAN, does not replace it

What Signals Does ODM Use?

Device Fingerprint

IP address, device model, OS version, screen resolution, language, carrier

Temporal Signals

Install timestamp, click timestamp, time decay weighting

Cohort Pattern Matching

Comparison against AppsFlyer's aggregate cohort signals from opted-in traffic

How ODM Works — End to End

📱
Install Event
User installs app. No IDFA consent. Device signals captured.
🧮
Signal Processing
Probabilistic engine matches device signals against known ad interaction patterns.
🏆
Attribution Output
Modelled attribution credit assigned. Surfaces in dashboard as "probabilistic."

How ODM Improves SKAN Measurement

🕳️

1. Fills Null CV Gaps

When SKAN returns a null conversion value (privacy threshold not met), ODM provides a modelled attribution signal so campaigns still receive measurable performance data.

⏱️

2. Extends Attribution Window

SKAN only covers install-level signal within defined windows. ODM extends attribution modelling to post-install events — providing LTV signal beyond Window 3 (day 35).

3. Cross-Validates SKAN Data

ODM probabilistic data validates whether SKAN postback volumes are consistent with expected campaign performance — flagging anomalies in SKAN reporting.

📈

4. Enables Incrementality Testing

ODM provides the baseline model needed to run incrementality tests — measuring true campaign lift against organic baseline, which pure SKAN data cannot do alone.

💰

5. Improves ROAS Accuracy

By recovering attribution for ~40% of ATT opt-out traffic, ODM significantly improves Return on Ad Spend calculations — preventing systematic under-reporting of paid performance.

🔮

6. Feeds LTV Prediction

ODM modelled conversions feed AppsFlyer's LTV prediction models — allowing pLTV to be calculated even for users acquired through dark traffic.

ODM vs SKAN — Complementary Not Competing

ODM and SKAN serve different parts of the iOS attribution picture. Use both together for complete coverage.

DimensionSKANODM (On Device Measurement)
Attribution typeDeterministic (Apple-validated)Probabilistic (modelled)
Data sourceApple-signed postbackDevice signal matching
User identityNone (aggregate only)None (device-level only)
CoverageATT opt-in + privacy threshold metATT opt-out traffic
Post-install eventsCV update only (windows 1–3)Extended modelled events
Privacy complianceApple-mandatedAppsFlyer privacy-safe
Ideal usePrimary attribution signalGap-filling and validation

Fine vs Coarse Conversion Values

SKAN 4.0 introduced a two-tier conversion value system. Fine values give granular 6-bit precision in Window 1; Coarse values provide a low/medium/high fallback across all three windows.

64Fine values (0–63, 6-bit)
3Coarse values (low/mid/high)
W1 OnlyFine values available
W1–W3Coarse values available

The Three Conversion Windows

Window 1
Days 0–2
Early post-install behaviour. Highest signal quality.
Fine + Coarse
Window 2
Days 3–7
Mid-term engagement. Revenue, sessions, key actions.
Coarse Only
Window 3
Days 8–35
Long-term retention, LTV, subscription conversion.
Coarse Only
🎯

Fine Conversion Value

Window 1 Only
Range0 – 63 (6-bit integer)
Precision64 distinct states
WindowDays 0–2 post-install
Privacy thresholdHigher — more installs required per campaign-source combo
When null?Privacy threshold not met; Apple sends coarse only or null
Update ruleCan only increase (monotone)
API callupdatePostbackConversionValue(_:coarseValue:lockWindow:)
📊

Coarse Conversion Value

All 3 Windows
Valueslow medium high
Precision3 discrete buckets
WindowsWindows 1, 2, and 3
Privacy thresholdLower bar — easier to receive than fine value
When received?Whenever threshold met; fallback when fine is withheld
AF mappinglow = CV 0–20 · medium = CV 21–42 · high = CV 43–63
API enumSKAdNetwork.CoarseConversionValue

Privacy Threshold — Why It Matters

Apple's crowd anonymity system withholds granular signals if a campaign-source combination has too few installs. The threshold is dynamic and based on Apple's privacy model.

🔴
Threshold Not Met

Apple sends null for both fine and coarse. No CV signal. High null CV rate in dashboard.

🟡
Coarse Threshold Met

Apple sends coarse only (low/medium/high). Fine value withheld. Partial signal.

🟢
Fine Threshold Met

Apple sends both fine (0–63) and coarse. Full signal available. Low null CV rate.

Fine Conversion Value Grid (0–63)

Hover any cell to see its value. Colour bands indicate user value tier.

0 — Install only
1–15 — Low value
16–31 — Mid value
32–47 — High value
48–63 — Premium

AppsFlyer: Fine → Coarse Mapping

Coarse BucketFine CV RangeTypical User BehaviourWindow Availability
lowCV 0–20Install only, minimal engagement, no purchaseW1 (fallback), W2, W3
mediumCV 21–42Active engagement, registration, add-to-cartW1 (fallback), W2, W3
highCV 43–63Purchase, subscription, high-value actionW1 (fallback), W2, W3

Reading the 6-Bit Fine Value

Each fine CV is a 6-bit binary number. Each bit represents a distinct user action or revenue band.

Fine CV Bit Schema Example
Bit 5 Bit 4 Bit 3 Bit 2 Bit 1 Bit 0 │ │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ ▼ Checkout AddCart SignUp MobileRev HBBRev ─── Revenue Band (2-bit) └─► 00=$0 01=$1-50 10=$51-100 11=$100+ Example: User signs up + spends $75 in mobile revenue Bit 5=0 Bit 4=0 Bit 3=1 Bit 2=1 Bit 1=1 Bit 0=0 Binary: 001110 → Decimal CV = 14 Coarse mapping: "low" (CV 14 < 21)

Flutter / Dart Implementation

Complete implementation handling all three scenarios: consent granted, consent denied, and runtime consent changes.

📱 SKAN Manager — Core Class

appsflyer_skan_manager.dart
import 'package:appsflyer_sdk/appsflyer_sdk.dart'; import 'package:app_tracking_transparency/app_tracking_transparency.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'dart:async'; import 'dart:io' show Platform; /// Complete AppsFlyer SKAN Manager /// Handles: Consent Granted (A), Consent Denied (B), Runtime Changes (C) class AppsFlyerSKANManager { static final AppsFlyerSKANManager _instance = AppsFlyerSKANManager._internal(); factory AppsFlyerSKANManager() => _instance; AppsFlyerSKANManager._internal(); AppsflyerSdk? _appsflyerSdk; bool _isInitialized = false; bool _hasConsent = false; bool _isStopped = false; TrackingStatus? _currentTrackingStatus; final _consentStateController = StreamController<bool>.broadcast(); Stream<bool> get consentStateStream => _consentStateController.stream; static const String AF_DEV_KEY = 'YOUR_APPSFLYER_DEV_KEY'; static const String APP_ID = 'YOUR_APP_STORE_ID'; Future<void> initialize() async { if (!Platform.isIOS || _isInitialized) return; await _checkAndHandleTrackingStatus(); _isInitialized = true; } Future<void> _checkAndHandleTrackingStatus() async { _currentTrackingStatus = await AppTrackingTransparency.trackingAuthorizationStatus; switch (_currentTrackingStatus) { case TrackingStatus.notDetermined: await _requestTrackingPermission(); break; case TrackingStatus.authorized: await _initializeWithConsent(); break; default: await _initializeWithoutConsent(); } } // ── SCENARIO A: CONSENT GRANTED ────────────────────────────────── Future<void> _initializeWithConsent() async { _hasConsent = true; _consentStateController.add(true); final options = AppsFlyerOptions( afDevKey: AF_DEV_KEY, appId: APP_ID, showDebug: true, timeToWaitForATTUserAuthorization: 10.0, disableIDFACollection: false, // IDFA ON disableAdvertisingIdentifier: false, disableCollectASA: false, // Apple Search Ads ON ); _appsflyerSdk = AppsflyerSdk(options); _appsflyerSdk!.initSdk( registerConversionDataCallback: true, registerOnAppOpenAttributionCallback: true, registerOnDeepLinkingCallback: true, ); _appsflyerSdk!.onInstallConversionData((data) { print('Attribution: ${data['af_status']}'); }); _appsflyerSdk!.startSDK(); _isStopped = false; } // ── SCENARIO B: CONSENT DENIED ─────────────────────────────────── Future<void> _initializeWithoutConsent() async { _hasConsent = false; _consentStateController.add(false); final options = AppsFlyerOptions( afDevKey: AF_DEV_KEY, appId: APP_ID, showDebug: false, timeToWaitForATTUserAuthorization: 0.0, disableIDFACollection: true, // IDFA OFF disableAdvertisingIdentifier: true, disableCollectASA: true, // ASA OFF ); _appsflyerSdk = AppsflyerSdk(options); _appsflyerSdk!.initSdk(registerConversionDataCallback: false); _appsflyerSdk!.anonymizeUser(true); // SKAN-only mode _appsflyerSdk!.startSDK(); _isStopped = false; } // ── SCENARIO C: RUNTIME CONSENT CHANGES ────────────────────────── Future<void> handleConsentChange(bool newConsentStatus) async { if (_hasConsent == newConsentStatus) return; _appsflyerSdk?.stop(true); _isStopped = true; await Future.delayed(Duration(milliseconds: 500)); if (newConsentStatus) { await _initializeWithConsent(); } else { _appsflyerSdk?.anonymizeUser(true); await _initializeWithoutConsent(); } } Future<void> logEvent(String eventName, Map<String, dynamic> params) async { if (_appsflyerSdk == null || _isStopped) return; if (_hasConsent) { _appsflyerSdk!.logEvent(eventName, params); } else { // Strip PII in SKAN-only mode final safe = Map.fromEntries( params.entries.where((e) => ['value','success','category','type'].contains(e.key)) ); _appsflyerSdk!.logEvent(eventName, safe); } } bool get hasConsent => _hasConsent; bool get isInitialized => _isInitialized; void dispose() { _consentStateController.close(); _appsflyerSdk?.stop(false); } }

📊 Event Tracker

event_tracker.dart
class EventTracker { final _manager = AppsFlyerSKANManager(); Future<void> trackPurchase({required double revenue, required String currency, required String productId, String? userId}) async { final params = <String, dynamic>{'af_revenue': revenue, 'af_currency': currency}; if (_manager.hasConsent) { params['af_content_id'] = productId; if (userId != null) params['customer_user_id'] = userId; } await _manager.logEvent('af_purchase', params); } Future<void> trackHBBRevenue(double amount) async { await _manager.logEvent('hbb_purchase', {'af_revenue': amount, 'af_currency': 'USD', 'channel': 'HBB'}); } Future<void> trackMobileRevenue(double amount) async { await _manager.logEvent('mobile_purchase', {'af_revenue': amount, 'af_currency': 'USD', 'platform': 'mobile'}); } Future<void> trackSignUp(String method, String? email) async { final params = <String, dynamic>{'af_registration_method': method}; if (_manager.hasConsent && email != null) { params['email_hash'] = email.hashCode.toString(); } await _manager.logEvent('af_complete_registration', params); } Future<void> trackAddToCart(String productId, double price) async { await _manager.logEvent('af_add_to_cart', { 'af_price': price, 'af_currency': 'USD', if (_manager.hasConsent) 'af_content_id': productId, }); } Future<void> trackCheckout(double total, int items) async { await _manager.logEvent('af_initiated_checkout', { 'af_price': total, 'af_currency': 'USD', 'af_quantity': items, }); } }

⚙️ Privacy Settings Screen

privacy_settings.dart
import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'appsflyer_skan_manager.dart'; class PrivacySettingsScreen extends StatefulWidget { @override _PrivacySettingsScreenState createState() => _PrivacySettingsScreenState(); } class _PrivacySettingsScreenState extends State<PrivacySettingsScreen> { final _manager = AppsFlyerSKANManager(); bool _trackingEnabled = false; bool _isLoading = false; @override void initState() { super.initState(); _loadSettings(); _manager.consentStateStream.listen((hasConsent) { if (mounted) setState(() => _trackingEnabled = hasConsent); }); } Future<void> _loadSettings() async { final prefs = await SharedPreferences.getInstance(); setState(() => _trackingEnabled = prefs.getBool('tracking_consent') ?? false); } Future<void> _handleConsentToggle(bool newValue) async { setState(() => _isLoading = true); final confirmed = await showDialog<bool>( context: context, builder: (ctx) => AlertDialog( title: Text(newValue ? 'Enable Full Tracking?' : 'Disable Tracking?'), content: Text(newValue ? 'Enable IDFA collection and full analytics.' : 'Switch to SKAN-only mode. No personal data collected.'), actions: [ TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text('Cancel')), ElevatedButton(onPressed: () => Navigator.pop(ctx, true), child: Text('Confirm')), ], ), ); if (confirmed == true) { final prefs = await SharedPreferences.getInstance(); await prefs.setBool('tracking_consent', newValue); await _manager.handleConsentChange(newValue); setState(() => _trackingEnabled = newValue); ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(newValue ? '✅ Full tracking enabled' : '🔒 SKAN-only mode active'), )); } setState(() => _isLoading = false); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Privacy Settings')), body: ListView( padding: EdgeInsets.all(16), children: [ Card(child: SwitchListTile( title: Text(_trackingEnabled ? 'Full Tracking Mode' : 'SKAN-Only Mode'), subtitle: Text(_trackingEnabled ? 'IDFA collection and full analytics enabled.' : 'Anonymous SKAN tracking. Privacy protected.'), value: _trackingEnabled, onChanged: _isLoading ? null : _handleConsentToggle, )), Card(child: Padding(padding: EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Mode Details', style: TextStyle(fontWeight: FontWeight.bold)), SizedBox(height: 8), Text('• Scenario A: Consent Granted → Full IDFA tracking'), Text('• Scenario B: Consent Denied → SKAN-only, anonymised'), Text('• Scenario C: Runtime toggle → Smooth SDK transition'), ], ))), ], ), ); } }

🚀 App Entry Point

main.dart
import 'package:flutter/material.dart'; import 'appsflyer_skan_manager.dart'; import 'event_tracker.dart'; import 'privacy_settings.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await AppsFlyerSKANManager().initialize(); runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'AppsFlyer SKAN Demo', home: HomeScreen(), ); } } class HomeScreen extends StatelessWidget { final _tracker = EventTracker(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('AppsFlyer SKAN Demo'), actions: [ IconButton(icon: Icon(Icons.privacy_tip), onPressed: () { Navigator.push(context, MaterialPageRoute(builder: (_) => PrivacySettingsScreen())); }), ], ), body: Center(child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton(onPressed: () => _tracker.trackPurchase(revenue: 99.99, currency: 'USD', productId: 'PRD123'), child: Text('Track Purchase')), SizedBox(height: 16), ElevatedButton(onPressed: () => _tracker.trackSignUp('email', 'user@example.com'), child: Text('Track Sign Up')), SizedBox(height: 16), ElevatedButton(onPressed: () => _tracker.trackAddToCart('PRD123', 29.99), child: Text('Track Add to Cart')), SizedBox(height: 16), ElevatedButton(onPressed: () => _tracker.trackHBBRevenue(49.99), child: Text('Track HBB Revenue')), ], )), ); } }

Dashboard Configuration

Step-by-step guide to configuring your SKAN conversion value schema in AppsFlyer's Conversion Studio.

Setup Steps

  1. Navigate to AppsFlyer → SKAN → Conversion Studio
  2. Click "Create New Schema" or edit existing
  3. Select measurement mode: Revenue or Engagement
  4. Map 6-bit schema (values 0–63) to your priority events
  5. Configure coarse bucket boundaries (low/medium/high thresholds)
  6. Set measurement window activity per window (W1, W2, W3)
  7. Validate in sandbox mode
  8. Deploy to production — allow 72h for schema propagation

Example 6-Bit Schema

Bits 0–1
Revenue Bands
$0 / $1–50 / $51–100 / $100+
Bit 2
HBB Revenue
Yes / No
Bit 3
Mobile Revenue
Yes / No
Bit 4
Sign Up
Completed / Not
Bit 5
Checkout
Initiated / Not

Campaign ID Constraints by Ad Network

SKAN allows Campaign IDs 1–100, but each network has its own constraints and best practices.

🔍
Google Ads
100 IDs
  • Structure: Flexible mapping by campaign type
  • Types: Search, Display, YouTube, App
  • Best practice: Group by campaign objective
📘
Meta (Facebook)
100 IDs
  • Structure: 3-digit hierarchical mapping
  • Mapping: Campaign (1st) · Ad Set (2nd) · Ad (3rd)
  • Best practice: 10 campaigns × 10 ad sets
🍎
Apple Search Ads
100 IDs
  • Structure: Campaign → Ad Group → Keyword
  • Advantage: Native SKAN integration
  • Best practice: Mirror ASA campaign structure
🎵
TikTok
100 IDs
  • Structure: Campaign-level mapping
  • Limitation: One ID per campaign
  • Best practice: Reserve IDs by objective type
👻
Snapchat
63 IDs
  • Structure: Campaign-based
  • Limitation: Lower than full SKAN range
  • Best practice: Focus on top campaigns only
📦
Amazon Ads
50–60 eff.
  • Types: Sponsored Products, Brands, Display
  • Limitation: DSP has separate tracking
  • Best practice: Segment by product line
🤖
Reddit
100 IDs
  • Structure: Promoted Posts / Takeover
  • Best practice: Reserve IDs by subreddit tier
  • Note: High null CV rate — consolidate campaigns
💹
The Trade Desk
100 IDs
  • Structure: Advertiser → Campaign → Ad Group
  • Feature: Cross-device DSP support
  • Best practice: Align with DSP hierarchy

Campaign ID Planning Best Practice

📋 ID Range Strategy

  • IDs 1–10: Top-performing always-on campaigns
  • IDs 11–50: Regular campaign rotation
  • IDs 51–100: Testing, seasonal, experimental
  • Document all assignments in a shared registry

🔄 Optimisation Tips

  • Reuse IDs after campaigns end (30-day recycle window)
  • Consolidate campaigns to reduce null CV rate
  • Monitor null CV % per network in SKAN dashboard
  • Adjust assignment per network performance data

Conversion Studio Configuration

How to configure AppsFlyer's Conversion Studio in Custom Encode Mode for a telco client — mapping 6 discrete purchase-funnel events to the 6-bit SKAN schema.

📌 CV 0 — Install

Reserved by Apple and AppsFlyer. Automatically set when no conversion event fires during the measurement window. Cannot be overridden.

⚙️ CV 1–63 — Custom Encode

All 6 bits freely assignable. In fixed/revenue mode bits 0–1 default to af_purchase revenue ranges. Custom encode mode replaces this with any event mapping.

📈 Monotone Rule

CV can only ever increase within a measurement window — never decrease. Schema should therefore order bits from lowest to highest business value.

Bit Assignment — As Configured in Conversion Studio

In Custom Encode mode, navigate to Settings → SKAN Conversion Studio → Custom and assign each bit to an in-app event. Below is the telco configuration for this schema.

Bit CV + In-App Event (Conversion Studio) Event Type Funnel Stage Fires when…
Bit 0 +1 af_add_to_cart Standard AF Event Top of funnel Customer adds any product to basket — earliest measurable intent signal
Bit 1 +2 af_initiated_checkout Standard AF Event Mid funnel Customer enters payment or personal details flow — high purchase intent
Bit 2 +4 electronics_order Custom Event Conversion Purchase of accessory, tablet, router, or smart home device — upsell signal
Bit 3 +8 handset_order Custom Event Conversion Mobile device purchased on a 24-month airtime contract — strong ARPU lock-in
Bit 4 +16 simo_order Custom Event Conversion SIM-only airtime plan taken — high-margin, recurring ARPU, low churn signal
Bit 5 +32 hbb_purchase Custom Event Highest value Home Broadband bundle completed — highest single-event CV contribution in schema

Priority Bundle View (Window 1 — Custom Encode Mode)

Conversion Studio groups bit assignments into priority bundles. In custom encode mode, each bit maps to one event — the priority order reflects ascending business value, with higher-value product events assigned to higher bits.

1
HBB Purchase — Bit 5 hbb_purchase · +32 CV · Highest single-event value
Highest Priority
🏠 hbb_purchase Custom event · 1 occurrence · Bit 5 set → CV +32
Home Broadband bundle completed within the measurement window. Firing Bit 5 alone produces CV = 32. Combined with SIMO or Handset (cross-sell within window) → CV reaches 48–63.
2
SIMO Order — Bit 4 simo_order · +16 CV · High-margin recurring revenue
High Priority
📶 simo_order Custom event · 1 occurrence · Bit 4 set → CV +16
SIM-only airtime plan taken. Firing Bit 4 alone → CV = 16. With Add to Cart + Checkout also fired (earlier in window) → CV = 19. Strong LTV indicator for ARPU modelling.
3
Handset Order — Bit 3 handset_order · +8 CV · Device + airtime contract
Medium-High Priority
📱 handset_order Custom event · 1 occurrence · Bit 3 set → CV +8
Mobile device on a 24-month contract. Bit 3 alone → CV = 8. With funnel entry events also set → CV = 11. High device ARPU and long contract term make this a strong LTV signal.
4
Electronics Order — Bit 2 electronics_order · +4 CV · Accessories & peripherals
Medium Priority
🖥️ electronics_order Custom event · 1 occurrence · Bit 2 set → CV +4
Tablet, router, smart home device, or broadband add-on purchased. Bit 2 alone → CV = 4. Lower ARPU than core products but indicates cross-sell propensity — valuable upsell signal.
5
Initiate Checkout — Bit 1 af_initiated_checkout · +2 CV · Pre-purchase intent
Low-Medium Priority
💳 af_initiated_checkout Standard AF event · 1 occurrence · Bit 1 set → CV +2
Customer entered the checkout flow. Standard AppsFlyer event name — no custom SDK mapping required. Bit 1 alone → CV = 2. Strong predictor of same-window purchase completion in telco.
6
Add to Cart — Bit 0 af_add_to_cart · +1 CV · Earliest intent signal
Lowest Priority
🛒 af_add_to_cart Standard AF event · 1 occurrence · Bit 0 set → CV +1
Customer added a product to basket. Standard AppsFlyer event. Bit 0 alone → CV = 1. Ensures CV ≥ 1 for any engaged user, distinguishing real sessions from null postbacks. Lowest CV weight — top-of-funnel signal only.

CV Value Map — All 63 States Decoded

Because each bit is independent and non-exclusive (except CV 0 which is reserved), all 63 combinations are valid. Key values to know for campaign optimisation:

0
Install only — no events fired
Reserved by Apple
1
Add to Cart only
000001
3
Cart + Checkout
000011
4
Electronics only
000100
8
Handset only
001000
11
Cart + Checkout + Handset
001011
16
SIMO only
010000
32
HBB only
100000
19
Cart + Checkout + SIMO
010011
35
Cart + Checkout + HBB
100011
48
HBB + SIMO cross-sell
110000
63
All 6 events — max LTV
111111

Coarse CV Mapping (Windows 2 & 3)

SKAN 4.0 sends coarse values (low / medium / high) in Windows 2 and 3 after the fine CV window closes. For this telco schema, the coarse thresholds should be configured in Conversion Studio as:

low
CV 1–7
Cart / Checkout / Electronics
Funnel entry — no product purchase yet
medium
CV 8–31
Handset or SIMO purchased
Core product conversion
high
CV 32–63
HBB purchase (± any other product)
Highest-value customer segment

Sample SKAN Dashboard — Media Source Performance

Media SourceInstallsNull CV RateCostRevenueSignal Quality
Organic333,823N/AN/A€1.74Baseline
Apple Search Ads7,5990.01%€14,607€1.71Excellent
TikTok1,1285.67%€2,985€1.71Good
Snapchat86410.65%€522€1.71Good
Google Ads2,92330.41%€7,824€0.03Moderate
Meta / Facebook1,60788.18%€835€0.03Low — Consolidate
⚠️ Meta Action Required: 88.18% null CV rate indicates Meta campaigns are below Apple's privacy threshold. Consolidate ad sets to increase per-campaign install volumes and recover conversion signal.

Technical Requirements

Implementation prerequisites, SDK references, and testing checklist.

TR1 — AppsFlyer Flutter SDK Integration
GitHub: AppsFlyerSDK/appsflyer-flutter-plugin

Add to pubspec.yaml: appsflyer_sdk: ^6.x.x
Minimum iOS deployment target: iOS 14.0+ for SKAN 4.0 support.
TR2 — SKAdNetwork Network IDs in Info.plist
All ad network SKAN IDs must be declared in Info.plist under SKAdNetworkItems.

Registry: github.com/skadnetwork/ad-network-ids

Key networks to include: Google, Meta, TikTok, Apple Search Ads, Amazon, Snapchat, Reddit.
TR3 — SKAN Postback Testing
Use Apple's SKAdNetwork test profile to reduce postback delays from 24–48h to seconds during development.

Documentation: AppsFlyer SKAN Testing Guide

Steps: Register test device → Enable SDK debug mode → Monitor CV updates → Validate postbacks in AF test dashboard.
TR4 — ATT Permission Flow
Implement App Tracking Transparency prompt before initialising the AppsFlyer SDK.

Package: app_tracking_transparency: ^3.x.x

Set timeToWaitForATTUserAuthorization to give sufficient time for the ATT dialog. Recommended: 10.0 seconds.
TR5 — Conversion Studio Configuration
Define conversion schema in AppsFlyer dashboard before going live. Schema changes take up to 72 hours to propagate (pending active CV window expiry).

Recommended: test schema in sandbox for minimum 48h before production deployment.
TR6 — ODM & ICM Configuration
Enable On Device Modelling and Integrated Conversion Management in the AppsFlyer dashboard under Settings → Attribution → Privacy-Preserving Attribution.

ICM requires: consent framework implementation, Conversion Studio schema active, and minimum 30-day data collection window for model training.

Interactive SKAN Simulator

Simulate a real telco customer journey and watch how each purchase event sets bits in the 6-bit conversion value schema — exactly as AppsFlyer encodes it for SKAN postbacks.

How This Simulator Works

1
Trigger a Purchase Event

Each button below represents a real post-install customer action — an HBB purchase, Handset order, or SIMO subscription.

2
Bits Flip in the Schema

AppsFlyer's SDK maps that event to a specific bit position in the 6-bit schema. The bit flips from 0 → 1 and the decimal CV increases.

3
SKAN Postback Is Encoded

After the window closes, Apple sends this CV to the ad network. AppsFlyer decodes it back to understand which products were purchased.

⚠️ Key SKAN Rule: The conversion value can only ever increase — never decrease. Once a bit is set to 1, it stays at 1. This is why event prioritisation in Conversion Studio matters.

Step 1 — Simulate the Telco Customer Funnel

Each event below maps to a single bit in the 6-bit schema — configured in AppsFlyer Conversion Studio custom encode mode. Trigger events in any order to build up the CV.

📌 Schema note: In Conversion Studio's fixed revenue mode, bits 0–1 are conventionally tied to af_purchase revenue ranges. In custom encode mode — used here — all 6 bits are freely assignable. This telco schema replaces that convention with a full purchase-funnel mapping across 6 discrete events, ordered by ascending value.
— SIMO Orders (Bits 0–1) —
📶
Bit 0 · +1 CV
SIMO 30-Day
Rolling SIM-Only contract

Customer takes a 30-day rolling SIM-only plan — no handset, no long-term commitment. Flexible entry-level product. Lowest CV weight but strong acquisition volume signal.

AF eventsimo_30d
Bit setBit 0 → binary 000001
CV aloneFiring this bit only → CV = 1
📶
Bit 1 · +2 CV
SIMO 2-Year
24-month committed contract

Customer commits to a 24-month SIM-only contract. Higher ARPU than 30-day, significantly lower churn. Strongest LTV signal in the SIMO category — preferred by UA optimisation models.

AF eventsimo_24mo
Bit setBit 1 → binary 000010
CV aloneFiring this bit only → CV = 2
🏠
Bit 2 · +4 CV
HBB Purchase — FTTC
Fibre-to-the-Cabinet broadband

Customer purchases a Fibre-to-the-Cabinet (FTTC) broadband plan — copper last mile, typically 30–80 Mbps. Lower revenue tier than full fibre but high adoption volume.

AF eventhbb_fttc
Bit setBit 2 → binary 000100
CV aloneFiring this bit only → CV = 4
— HBB Full Fibre & Handset Orders (Bits 3–5) —
🏠
Bit 3 · +8 CV
HBB Purchase — FTTP
Full Fibre to the Premises

Customer purchases a full-fibre FTTP plan — fibre all the way to the property, typically 150–1000 Mbps. Higher ARPU than FTTC, strong retention due to superior product quality.

AF eventhbb_fttp
Bit setBit 3 → binary 001000
CV aloneFiring this bit only → CV = 8
📱
Bit 4 · +16 CV
Handset — Mid Range
£200–£500 device + contract

Customer orders a mid-range handset (e.g. Samsung Galaxy A-series, iPhone SE) on a 24-month device plan. Solid ARPU — contract lock-in makes this a reliable LTV predictor.

AF eventhandset_mid
Bit setBit 4 → binary 010000
CV aloneFiring this bit only → CV = 16
📱
Bit 5 · +32 CV
Handset — Premium
£800+ flagship device + contract

Customer orders a flagship handset (e.g. iPhone Pro, Samsung Galaxy S-series) on a premium device plan. Highest single-bit CV — strong revenue, high ARPU, and strong brand loyalty retention signal.

AF eventhandset_premium
Bit setBit 5 → binary 100000
CV aloneFiring this bit only → CV = 32

Step 2 — Live Conversion Value Readout

This updates in real time as you trigger events above. This is exactly what AppsFlyer encodes into the SKAN postback sent to Apple.

Fine CV (0–63)
0
Decimal value sent in SKAN postback
6-Bit Binary
000000
Each bit = one product/revenue signal
Coarse Bucket
Windows 2 & 3 fallback
Products Active
None
Bits set across 3 products
6-Bit Schema — Live State (MSB → LSB)
Bit 5 · +32
0
Handset Prem.
Bit 4 · +16
0
Handset Mid
Bit 3 · +8
0
HBB FTTP
Bit 2 · +4
0
HBB FTTC
Bit 1 · +2
0
SIMO 2yr
Bit 0 · +1
0
SIMO 30D
Custom encode mode — all 6 bits freely assigned. Bits shown MSB→LSB (left to right). Each event sets exactly one bit independently.
📋 Event Log
No events fired yet. Trigger a purchase event above to begin the simulation.

Step 3 — What This CV Tells AppsFlyer

When AppsFlyer receives the SKAN postback with this CV, here is how it decodes and uses the data for campaign optimisation.

🔍

Postback Interpretation

Waiting for your first event trigger…

Trigger one or more purchase events above and the interpretation will update here in real time.

Full 6-Bit Schema Reference

The complete mapping of bits to events for this telco SKAN schema. AppsFlyer's Conversion Studio encodes this in the dashboard before deployment.

Bit PositionDecimal ValueEvent / SignalTrigger ConditionBusiness Meaning
Bit 0+1 SIMO 30-Day simo_30d 30-day rolling SIM-only contract. Flexible, no lock-in. Firing alone → CV = 1. Lowest CV weight — high volume, lower ARPU than committed plan.
Bit 1+2 SIMO 2-Year simo_24mo 24-month committed SIM-only contract. Higher ARPU, lower churn. Firing alone → CV = 2. With 30-day also set (rare) → CV = 3.
Bit 2+4 HBB FTTC hbb_fttc Fibre-to-the-Cabinet broadband (30–80 Mbps, copper last mile). Firing alone → CV = 4. Lower revenue tier than FTTP but strong volume. With SIMO 2yr → CV = 6.
Bit 3+8 HBB FTTP hbb_fttp Full Fibre to the Premises (150–1000 Mbps). Higher ARPU and retention vs FTTC. Firing alone → CV = 8. Premium broadband — strongest HBB LTV signal.
Bit 4+16 Handset Mid Range handset_mid Mid-range device (£200–£500, e.g. Samsung A-series, iPhone SE) on 24-month plan. Firing alone → CV = 16. Reliable ARPU, contract lock-in = strong LTV predictor.
Bit 5+32 Handset Premium handset_premium Flagship device (£800+, e.g. iPhone Pro, Galaxy S-series) on premium plan. Firing alone → CV = 32. Highest single-bit contribution. Full bundle (all 6 bits) → CV = 63.

Why This Schema Design Matters

🎯
Product-Level Attribution

Each product type has its own bit, so the ad network knows exactly which product drove the conversion — not just that a conversion happened.

📈
Bid Strategy Optimisation

Google and Meta use the CV to optimise bids toward users likely to order Handsets (CV 8+) or HBB (CV 4+) rather than just any install.

🔒
Privacy Preserved

No individual customer is identified — Apple sends only the aggregate CV number. AppsFlyer decodes what product signals it contains.