SKAdNetwork 4.0 Implementation Guide
Implement Apple's SKAdNetwork 4.0 framework for privacy-focused iOS attribution using server-to-server integration, enabling measurement of app install campaigns with enhanced postback windows and coarse conversion values while protecting user privacy.
Overview
What is SKAdNetwork 4.0?
SKAdNetwork (SKAN) is Apple's privacy-focused attribution framework enabling measurement of iOS app install advertising campaigns while protecting user privacy through server-to-server implementation for robust validation and tracking.
The framework handles all critical aspects of attribution while maintaining user privacy through Apple's prescribed methods, making it essential for mobile marketers operating in the post-iOS 14.5 landscape.
Key Features
SKAdNetwork 4.0 introduces enhanced measurement capabilities and flexibility for campaign optimization.
- Robust Validation: Aggregation of postbacks from all networks with built-in fraud protection
- Dynamic Conversion Management: Dashboard configuration for conversion value encoding
- Enhanced Reporting: Enriched marketing parameters and granular data insights
- Secure Partner Postbacks: Decoded conversion values and revenue tracking
- Comprehensive Validation: Enriched event and session tracking for implementation verification
- Multiple Postback Windows: Automated timestamp management across 0-2 days, 3-7 days, 8-35 days
- Revenue Tracking: Support for ad monetization and regular revenue with currency specifications
Prerequisites
Familiarize yourself with SKAdNetwork concepts and previous versions before implementing SKAN 4.0.
- Learn about SKAdNetwork 4.0 in the SKAN 4.0 FAQ
- For background about SKAdNetwork, see the SKAdNetwork 3.0 S2S Implementation Guide
Singular SKAdNetwork Solution
Singular's SKAdNetwork solution provides end-to-end attribution management from client-side implementation to postback processing and campaign optimization.
Solution Components
Platform Features
Comprehensive SKAdNetwork support across the attribution and analytics workflow.
| Component | Functionality |
|---|---|
| Client-Side Code | Native iOS code samples for SKAdNetwork framework registration and conversion value management. Alternative server-side approach available using Conversion Value API endpoint |
| Postback Processing | Validation and aggregation of postbacks from all ad networks with unified reporting |
| Fraud Protection | Cryptographic signature validation, transaction ID deduplication, and secure parameter verification for unsigned data |
| Conversion Management | Dynamic dashboard configuration for encoding post-install activities into conversion values |
| Reporting | Campaign ID translation and enrichment with marketing parameters for granular analysis |
| Partner Postbacks | Decoded conversion values sent as events and revenue for partner optimization |
Implementation Architecture
Two-Part Implementation
Client-side SKAdNetwork implementation consists of two main components.
1. Client-Side Implementation (Required):
- SKAdNetwork framework registration on app launch
- Intelligent conversion value management based on post-install activity
- Essential for campaign optimization using SKAdNetwork attribution
- Enables tracking of associated post-install activities
2. Server-Side Integration Update (Recommended):
Client-Side Implementation
Implement SKAdNetwork framework registration and conversion value management using Singular's native iOS code samples for optimal campaign measurement with SKAN 4.0 features.
Implementation Responsibilities
Core Functionality
Code samples support SKAdNetwork registration and intelligent conversion value management.
- SKAdNetwork Registration: Registers app with framework immediately after launch to enable attribution
-
Conversion Value Management:
- Communicates synchronously with Singular endpoint to receive next conversion value
- Reports sessions, events, and revenue to Singular
- Receives encoded conversion value representing configured post-install activity
- Collects SKAdNetwork metadata by measurement period for validation and calculation
Metadata Collection
Code collects essential SKAdNetwork metadata for both validation and conversion value calculation.
- First call timestamp to underlying SKAdNetwork framework
- Last call timestamp to underlying SKAdNetwork framework
- Last updated postback values (both Coarse and Fine)
- Total Revenue and Total Ad Monetization Revenue generated by device
Integration Flow
End-to-End Process
Complete SKAdNetwork flow for S2S customers from client-side conversion management to postback processing.
- Conversion Value Request: App code communicates with Singular endpoint synchronously to get latest conversion value based on sessions, events, and revenue
- Framework Update: App updates SKAdNetwork framework with received conversion value
- Metadata Enrichment: App enriches S2S events and sessions with SKAdNetwork metadata for validation
- Timer Expiration: After timer expires, SKAdNetwork sends postback to ad network
- Postback Forwarding: Network forwards postback to Singular (secure setup or regular)
-
Postback Processing: Singular processes postback
by:
- Validating cryptographic signature
- Decoding conversion value using configured model
- Enriching with network information from partner integrations
- Sending decoded data to BI and partners via postbacks
Data Separation: SKAdNetwork data (installs and decoded events) accessible via separate reports, APIs, ETL tables, and postbacks to prevent mixing with existing datasets during testing and validation.
SKAdNetwork Interface
Complete interface definition for SKAdNetwork integration providing methods for attribution tracking, conversion value updates, and revenue management.
Method Definitions
Attribution Registration
Initializes SKAN attribution tracking on first app launch, setting initial conversion value to 0 and establishing baseline timestamps.
+ (void)registerAppForAdNetworkAttribution;
Conversion Value Management
Methods update conversion values based on post-install activity captured by app and selected conversion model configured dynamically.
Supported Activities:
- Sessions: Critical for retention measurement
- Conversion Events: Critical for post-install event measurement
- Revenue Events: Critical for revenue measurement
Session Tracking
Manages session-based tracking for retention and cohort analysis with optional completion handler for post-update actions.
+ (void)updateConversionValuesAsync:(void(^)(NSNumber *, NSNumber *, BOOL, NSError *))handler;
Event Tracking
Handles conversion event tracking before sending data to Singular, updating conversion values based on event context.
+ (void)updateConversionValuesAsync:(NSString *)eventName
withCompletionHandler:(void(^)(NSNumber *, NSNumber *, BOOL, NSError *))handler;
Revenue Management
Tracks revenue events, maintaining separate totals for ad monetization and regular revenue. Must be called before conversion value updates.
+ (void)updateRevenue:(double)amount
andCurrency:(NSString *)currency
isAdMonetization:(BOOL)admon;
Data Retrieval
Returns comprehensive SKAN data dictionary including conversion values, timestamps, and revenue tracking.
Dictionary Contains:
- Current and previous fine-grained conversion values
- Coarse values across different postback windows
- Window lock timestamps
- Revenue tracking by currency
- Separate tracking for ad monetization and regular revenue
+ (NSDictionary *)getSkanDetails;
Implementation Notes
- Methods use asynchronous patterns to prevent blocking main thread
- Revenue tracking must precede conversion value updates
- Supports both fine-grained (0-63) and coarse (Low/Medium/High) conversion values
- Maintains separate tracking for different postback windows
- Implements comprehensive error handling through completion handlers
Complete Interface Code
SKANSnippet.h
//SKANSnippet.h
#import <Foundation/Foundation.h>
@interface SKANSnippet : NSObject
// Register for SKAdNetwork attribution.
// Call this method as soon as possible on first app launch.
// Sets conversion value to 0 and updates timestamp for additional processing.
+ (void)registerAppForAdNetworkAttribution;
// Track retention and cohorts by calling for each app open.
// Reports session details and updates conversion value if needed.
// Optional callback runs once conversion value is updated.
+ (void)updateConversionValuesAsync:(void(^)(NSNumber *, NSNumber *, BOOL, NSError *))handler;
// Track conversion events by calling after each event and before sending to Singular.
// Reports event details and updates conversion value if needed.
// Optional callback runs once conversion value is updated.
+ (void)updateConversionValuesAsync:(NSString *)eventName
withCompletionHandler:(void(^)(NSNumber *, NSNumber *, BOOL, NSError *))handler;
// Track revenue by calling before every revenue event.
// Updates total revenue for next conversion value calculation.
// Note:
// 1. Call before 'updateConversionValuesAsync' to ensure revenue included
// 2. Avoid calling on event retries to prevent double-counting
+ (void)updateRevenue:(double)amount
andCurrency:(NSString *)currency
isAdMonetization:(BOOL)admon;
// Gets current fine, coarse, window locked values saved in dictionary.
// Contains all relevant SKAN values including:
// - skan_current_conversion_value
// - prev_fine_value
// - skan_first_call_to_skadnetwork_timestamp
// - skan_last_call_to_skadnetwork_timestamp
// - skan_total_revenue_by_currency
// - skan_total_admon_revenue_by_currency
// - p0_coarse, p1_coarse, p2_coarse
// - p0_window_lock, p1_window_lock, p2_window_lock
// - Previous coarse values and revenue per window
+ (NSDictionary *)getSkanDetails;
@end
SKAdNetwork Implementation
Complete implementation code for Apple's SKAdNetwork 4.0 interface, managing attribution tracking, conversion values, and revenue reporting across multiple postback windows.
Implementation Overview
Constants and Configuration
Implementation defines three distinct postback windows for tracking user activity and conversions.
static NSInteger firstSkan4WindowInSec = 3600 * 24 * 2; // 48 hours
static NSInteger secondSkan4WindowInSec = 3600 * 24 * 7; // 7 days
static NSInteger thirdSkan4WindowInSec = 3600 * 24 * 35; // 35 days
Key Features
- Attribution Registration: Handles initial app attribution setup and first-time conversion value tracking
- Conversion Management: Updates and tracks conversion values across multiple postback windows
- Revenue Tracking: Maintains separate tracking for ad monetization and regular revenue events
- Data Persistence: Uses NSUserDefaults to store SKAN-related data across app sessions
- Thread Safety: Implements NSLock for thread-safe operations during network calls
Data Storage Structure
- Fine-grained conversion values (0-63)
- Coarse values (Low/Medium/High mapped to 0-2)
- Revenue tracking by currency for each postback window
- Timestamp management for postback windows and lock states
- Previous value tracking for both fine and coarse conversions
Privacy Considerations
- Implements iOS 15.4+ and iOS 16.1+ specific features
- Handles postback conversion value updates according to Apple's privacy guidelines
- Maintains separate tracking for different revenue types to ensure accurate attribution
Technical Notes
- Uses asynchronous operations for network calls and value updates
- Implements error handling and validation for conversion values
- Supports both traditional and coarse conversion value tracking
- Manages multiple postback windows with different durations and requirements
Complete Implementation Code
SKANSnippet.m
Important: Replace placeholder values (YOUR API KEY, YOUR APP VERSION, etc.) with actual values from your application before production use.
// SKANSnippet.m
#import "SKANSnippet.h"
#import <StoreKit/SKAdNetwork.h>
#import <UIKit/UIKit.h>
#define SESSION_EVENT_NAME @"__SESSION__"
#define SINGULAR_API_URL @"https://sdk-api-v1.singular.net/api/v2/conversion_value"
// SKAN Keys for NSUserDefaults persistency and requests
#define CONVERSION_VALUE_KEY @"skan_current_conversion_value"
#define FIRST_SKAN_CALL_TIMESTAMP @"skan_first_call_to_skadnetwork_timestamp"
#define LAST_SKAN_CALL_TIMESTAMP @"skan_last_call_to_skadnetwork_timestamp"
#define TOTAL_REVENUE_BY_CURRENCY @"skan_total_revenue_by_currency"
#define TOTAL_ADMON_REVENUE_BY_CURRENCY @"skan_total_admon_revenue_by_currency"
#define SKAN_UPDATED_CONVERSION_VALUE @"conversion_value"
#define SKAN_UPDATED_COARSE_VALUE @"skan_updated_coarse_value"
#define SKAN_UPDATED_LOCK_WINDOW_VALUE @"skan_updated_lock_window_value"
#define P0_COARSE @"p0_coarse"
#define P1_COARSE @"p1_coarse"
#define P2_COARSE @"p2_coarse"
#define P0_WINDOW_LOCK_TS @"p0_window_lock"
#define P1_WINDOW_LOCK_TS @"p1_window_lock"
#define P2_WINDOW_LOCK_TS @"p2_window_lock"
#define P0_PREV_FINE_VALUE @"prev_fine_value"
#define P0_PREV_COARSE_VALUE @"p0_prev_coarse_value"
#define P1_PREV_COARSE_VALUE @"p1_prev_coarse_value"
#define P2_PREV_COARSE_VALUE @"p2_prev_coarse_value"
#define TOTAL_REVENUE_P0 @"p0_total_iap_revenue"
#define TOTAL_REVENUE_P1 @"p1_total_iap_revenue"
#define TOTAL_REVENUE_P2 @"p2_total_iap_revenue"
#define TOTAL_ADMON_REVENUE_P0 @"p0_total_admon_revenue"
#define TOTAL_ADMON_REVENUE_P1 @"p1_total_admon_revenue"
#define TOTAL_ADMON_REVENUE_P2 @"p2_total_admon_revenue"
@implementation SKANSnippet
static NSInteger firstSkan4WindowInSec = 3600 * 24 * 2; //48 hours in sec
static NSInteger secondSkan4WindowInSec = 3600 * 24 * 7;
static NSInteger thirdSkan4WindowInSec = 3600 * 24 * 35;
static NSLock *lockObject;
+ (void)registerAppForAdNetworkAttribution {
if ([SKANSnippet getFirstSkanCallTimestamp] != 0) {
return;
}
if (@available(iOS 15.4, *)) {
[SKAdNetwork updatePostbackConversionValue:0 completionHandler:nil];
[SKANSnippet setFirstSkanCallTimestamp];
[SKANSnippet setLastSkanCallTimestamp];
[SKANSnippet valuesHasBeenUpdated:@(0) coarseValue:nil lockWindow:NO];
}
}
+ (void)updateConversionValuesAsync:(void(^)(NSNumber *, NSNumber *, BOOL, NSError *))handler {
[SKANSnippet updateConversionValuesAsync:SESSION_EVENT_NAME withCompletionHandler:handler];
}
+ (void)updateConversionValuesAsync:(NSString *)eventName withCompletionHandler:(void(^)(NSNumber *, NSNumber *, BOOL, NSError *))handler {
if ([SKANSnippet isSkanWindowOver]) {
return;
}
[SKANSnippet getConversionValueFromServer:eventName withCompletionHandler:handler];
}
+ (void)updateRevenue:(double)amount andCurrency:(NSString *)currency isAdMonetization:(BOOL)admon {
// Update total revenues
if (amount == 0 || !currency ) {
return;
}
[SKANSnippet addToTotalRevenue:@(amount) withCurrency:currency isAdmon:admon];
}
+ (NSDictionary *)getSkanDetails {
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
NSMutableDictionary *res = [NSMutableDictionary dictionary];
//current fine
[res setValue:[[userDefaults valueForKey:CONVERSION_VALUE_KEY] stringValue] forKey:CONVERSION_VALUE_KEY];
//prev fine
[res setValue:[[userDefaults valueForKey:P0_PREV_FINE_VALUE] stringValue] forKey:P0_PREV_FINE_VALUE];
//current coarse
[res setValue:[[userDefaults valueForKey:P0_COARSE] stringValue] forKey:P0_COARSE];
[res setValue:[[userDefaults valueForKey:P1_COARSE] stringValue] forKey:P1_COARSE];
[res setValue:[[userDefaults valueForKey:P2_COARSE] stringValue] forKey:P2_COARSE];
//prev coarse
[res setValue:[[userDefaults valueForKey:P0_PREV_COARSE_VALUE] stringValue] forKey:P0_PREV_COARSE_VALUE];
[res setValue:[[userDefaults valueForKey:P1_PREV_COARSE_VALUE] stringValue] forKey:P1_PREV_COARSE_VALUE];
[res setValue:[[userDefaults valueForKey:P2_PREV_COARSE_VALUE] stringValue] forKey:P2_PREV_COARSE_VALUE];
//lock windows ts
[res setValue:[[userDefaults valueForKey:P0_WINDOW_LOCK_TS] stringValue] forKey:P0_WINDOW_LOCK_TS];
[res setValue:[[userDefaults valueForKey:P1_WINDOW_LOCK_TS] stringValue] forKey:P1_WINDOW_LOCK_TS];
[res setValue:[[userDefaults valueForKey:P2_WINDOW_LOCK_TS] stringValue] forKey:P2_WINDOW_LOCK_TS];
//total revenues
[res setValue:[userDefaults valueForKey:TOTAL_REVENUE_BY_CURRENCY] forKey:TOTAL_REVENUE_BY_CURRENCY];
[res setValue:[userDefaults valueForKey:TOTAL_ADMON_REVENUE_BY_CURRENCY] forKey:TOTAL_ADMON_REVENUE_BY_CURRENCY];
//revenue per window
[res setValue:[userDefaults valueForKey:TOTAL_REVENUE_P0] forKey:TOTAL_REVENUE_P0];
[res setValue:[userDefaults valueForKey:TOTAL_REVENUE_P1] forKey:TOTAL_REVENUE_P1];
[res setValue:[userDefaults valueForKey:TOTAL_REVENUE_P2] forKey:TOTAL_REVENUE_P2];
[res setValue:[userDefaults valueForKey:TOTAL_ADMON_REVENUE_P0] forKey:TOTAL_ADMON_REVENUE_P0];
[res setValue:[userDefaults valueForKey:TOTAL_ADMON_REVENUE_P1] forKey:TOTAL_ADMON_REVENUE_P1];
[res setValue:[userDefaults valueForKey:TOTAL_ADMON_REVENUE_P2] forKey:TOTAL_ADMON_REVENUE_P2];
//skan TS
[res setValue:[[userDefaults valueForKey:LAST_SKAN_CALL_TIMESTAMP] stringValue] forKey:LAST_SKAN_CALL_TIMESTAMP];
[res setValue:[[userDefaults valueForKey:FIRST_SKAN_CALL_TIMESTAMP] stringValue] forKey:FIRST_SKAN_CALL_TIMESTAMP];
return res;
}
#pragma mark - internal
+ (BOOL)validateValues:(NSNumber *)conversionValue coarse:(NSNumber *)coarseValue{
if ([conversionValue intValue] < 0 || 63 < [conversionValue intValue]) {
return NO;
}
if (coarseValue) {
if ([coarseValue intValue] > 2 || [coarseValue intValue] < 0) {
return NO;
}
}
return YES;
}
+ (NSURLComponents *)prepareQueryParams:(NSString *)bundleIdentifier eventName:(NSString *)eventName {
NSURLComponents *components = [NSURLComponents componentsWithString:SINGULAR_API_URL];
NSString *API_KEY = @"YOUR API KEY";
NSString *APP_VERSION = @"YOUR APP VERSION";
NSString *IDFV = @"IDFV";
NSString *IDFA = @"IDFA";
NSMutableArray *queryItems = [@[
[NSURLQueryItem queryItemWithName:@"a" value:API_KEY],
[NSURLQueryItem queryItemWithName:@"v" value:[[UIDevice currentDevice] systemVersion]],
[NSURLQueryItem queryItemWithName:@"i" value:bundleIdentifier],
[NSURLQueryItem queryItemWithName:@"app_v" value:APP_VERSION],
[NSURLQueryItem queryItemWithName:@"n" value:eventName],
[NSURLQueryItem queryItemWithName:@"p" value:@"iOS"],
[NSURLQueryItem queryItemWithName:@"idfv" value:IDFV],
[NSURLQueryItem queryItemWithName:@"idfa" value:IDFA]
] mutableCopy];
NSDictionary *skanValues = [SKANSnippet getSkanDetails];
[skanValues enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
if ([obj isKindOfClass:[NSDictionary class]]) {
[queryItems addObject:[NSURLQueryItem queryItemWithName:key value:[SKANSnippet dictionaryToJsonString:obj]]];
} else {
[queryItems addObject:[NSURLQueryItem queryItemWithName:key value:obj]];
}
}];
components.queryItems = queryItems;
return components;
}
+ (void)getConversionValueFromServer:(NSString*)eventName withCompletionHandler:(void(^)(NSNumber *, NSNumber *, BOOL, NSError*))handler {
if (!lockObject) {
lockObject = [NSLock new];
}
@try {
// Making the lock async so it will not freeze the calling thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
[lockObject lock];
NSString *bundleIdentifier = @"YOUR BUNDLE IDENTIFIER";
NSURLComponents *components = [SKANSnippet prepareQueryParams:bundleIdentifier eventName:eventName];
[[[NSURLSession sharedSession] dataTaskWithURL:components.URL
completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
[lockObject unlock];
if (handler) {
handler(nil, nil, NO, error);
}
return;
}
NSDictionary *parsedResponse = [SKANSnippet jsonDataToDictionary:data];
if (!parsedResponse) {
[lockObject unlock];
if (handler) {
handler(nil,nil, NO, [NSError errorWithDomain:bundleIdentifier
code:0
userInfo:@{NSLocalizedDescriptionKey:@"Failed parsing server response"}]);
}
return;
}
NSNumber *conversionValue = [parsedResponse objectForKey:SKAN_UPDATED_CONVERSION_VALUE];
NSNumber *coarseValue = [parsedResponse objectForKey:SKAN_UPDATED_COARSE_VALUE];
BOOL lockWindow = [[parsedResponse objectForKey:SKAN_UPDATED_LOCK_WINDOW_VALUE] boolValue];
if (!conversionValue) {
[lockObject unlock];
NSString *status = [parsedResponse objectForKey:@"status"];
if (!status || ![status isEqualToString:@"ok"]) {
if (handler) {
NSString *reason = [parsedResponse objectForKey:@"reason"];
if (!reason) {
reason = @"Got error from server";
}
handler(nil, nil, NO, [NSError errorWithDomain:bundleIdentifier
code:0
userInfo:@{NSLocalizedDescriptionKey:reason}]);
}
}
return;
}
if(![SKANSnippet validateValues:conversionValue coarse:coarseValue]) {
[lockObject unlock];
if (handler) {
handler(nil,nil, NO, [NSError errorWithDomain:bundleIdentifier
code:0
userInfo:@{NSLocalizedDescriptionKey:@"Illegal values received"}]);
}
return;
}
if (![SKANSnippet getFirstSkanCallTimestamp]) {
[SKANSnippet setFirstSkanCallTimestamp];
}
[SKANSnippet setConversionValues:conversionValue coarseValue:coarseValue lockWindow:lockWindow handler:handler];
[lockObject unlock];
}] resume];
});
} @catch (id exception) {
NSLog(@"%@", exception);
}
}
+ (void)setFirstSkanCallTimestamp {
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setInteger:[SKANSnippet getCurrentUnixTimestamp] forKey:FIRST_SKAN_CALL_TIMESTAMP];
}
+ (void)setLastSkanCallTimestamp {
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setInteger:[SKANSnippet getCurrentUnixTimestamp] forKey:LAST_SKAN_CALL_TIMESTAMP];
}
+ (NSString*)dictionaryToJsonString:(NSDictionary*)dictionary {
if (!dictionary || [dictionary count] == 0){
return @"{}";
}
NSError *error;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dictionary
options:0
error:&error];
if (error || !jsonData) {
return @"{}";
}
return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
}
+ (NSInteger)getFirstSkanCallTimestamp {
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
return [userDefaults integerForKey:FIRST_SKAN_CALL_TIMESTAMP];
}
+ (NSInteger)getLastSkanCallTimestamp {
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
return [userDefaults integerForKey:LAST_SKAN_CALL_TIMESTAMP];
}
+ (NSInteger)getCurrentUnixTimestamp {
return [[NSDate date] timeIntervalSince1970];
}
+ (void)setConversionValues:(NSNumber *)conversionValue coarseValue:(NSNumber *)coarse lockWindow:(BOOL)lockWindow handler:(void(^)(NSNumber *, NSNumber *, BOOL, NSError*))handler {
@try {
__block void(^skanResultHandler)(NSError * _Nullable error) = ^(NSError * _Nullable error) {
if (handler) {
if (error) {
handler(nil, nil, NO, error);
} else {
handler(conversionValue, coarse, lockWindow, nil);
}
}
[SKANSnippet valuesHasBeenUpdated:conversionValue coarseValue:coarse lockWindow:lockWindow];
};
if (@available(iOS 16.1, *)) {
[SKAdNetwork updatePostbackConversionValue:[conversionValue integerValue] coarseValue:[SKANSnippet resolveCoarseValueFrom:coarse] lockWindow:lockWindow completionHandler:^(NSError * _Nullable error) {
skanResultHandler(error);
}];
} else {
if (@available(iOS 15.4, *)) {
[SKAdNetwork updatePostbackConversionValue:[conversionValue integerValue] completionHandler:^(NSError * _Nullable error) {
skanResultHandler(error);
}];
}
}
} @catch (id exception) {
NSLog(@"%@", exception);
}
}
+ (NSNumber *)getConversionValue {
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
if (![userDefaults objectForKey:CONVERSION_VALUE_KEY]) {
return @(0);
}
return @([userDefaults integerForKey:CONVERSION_VALUE_KEY]);
}
+ (NSDictionary*)jsonDataToDictionary:(NSData*)jsonData {
if (!jsonData) {
return nil;
}
NSError *error;
NSDictionary *parsedData = [NSJSONSerialization JSONObjectWithData:jsonData
options:kNilOptions error:&error];
if (error || !parsedData) {
return nil;
}
return parsedData;
}
+ (NSInteger)getCurrentSkanWindow {
NSInteger timeDiff = [SKANSnippet getCurrentUnixTimestamp] - [SKANSnippet getFirstSkanCallTimestamp];
if (timeDiff < firstSkan4WindowInSec) { return 0; }
if (timeDiff < secondSkan4WindowInSec) { return 1; }
if (timeDiff < thirdSkan4WindowInSec) { return 2; }
return -1;
}
// persist updated conversion values based on the active skan window.
+ (void)valuesHasBeenUpdated:(NSNumber *)fineValue coarseValue:(NSNumber *)coarseValue lockWindow:(BOOL)lockWindow {
NSNumber *currentPersistedFineValue;
NSNumber *currentPersistedCoarseValue;
NSInteger window = [SKANSnippet getCurrentSkanWindow];
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
switch (window) {
case 0:
currentPersistedFineValue = [userDefaults objectForKey:CONVERSION_VALUE_KEY];
currentPersistedCoarseValue = [userDefaults objectForKey:P0_COARSE];
[userDefaults setValue:fineValue forKey:CONVERSION_VALUE_KEY];
[userDefaults setValue:currentPersistedFineValue forKey:P0_PREV_FINE_VALUE];
[userDefaults setValue:coarseValue forKey:P0_COARSE];
[userDefaults setValue:currentPersistedCoarseValue forKey:P0_PREV_COARSE_VALUE];
if (lockWindow) {
[userDefaults setObject:@([SKANSnippet getCurrentUnixTimestamp]) forKey:P0_WINDOW_LOCK_TS];
}
break;
case 1:
currentPersistedCoarseValue = [userDefaults objectForKey:P1_COARSE];
[userDefaults setValue:coarseValue forKey:P1_COARSE];
[userDefaults setValue:currentPersistedCoarseValue forKey:P1_PREV_COARSE_VALUE];
if (lockWindow) {
[userDefaults setObject:@([SKANSnippet getCurrentUnixTimestamp]) forKey:P1_WINDOW_LOCK_TS];
}
break;
case 2:
currentPersistedCoarseValue = [userDefaults objectForKey:P2_COARSE];
[userDefaults setValue:coarseValue forKey:P2_COARSE];
[userDefaults setValue:currentPersistedCoarseValue forKey:P2_PREV_COARSE_VALUE];
if (lockWindow) {
[userDefaults setValue:@([SKANSnippet getCurrentUnixTimestamp]) forKey:P2_WINDOW_LOCK_TS];
}
break;
}
[SKANSnippet setLastSkanCallTimestamp];
}
+ (BOOL)isSkanWindowOver {
NSInteger timeDiff = [SKANSnippet getCurrentUnixTimestamp] - [SKANSnippet getFirstSkanCallTimestamp];
return thirdSkan4WindowInSec <= timeDiff;
}
// Revenues are being accumulated and saved by Ad monetization and non ad monetization events, total sum and break down by skan windows.
+ (void)addToTotalRevenue:(NSNumber *)newRevenue withCurrency:(NSString *)currency isAdmon:(BOOL)isAdmon {
NSString *key = isAdmon ? TOTAL_ADMON_REVENUE_BY_CURRENCY : TOTAL_REVENUE_BY_CURRENCY;
[SKANSnippet addToTotalRevenue:newRevenue withCurrency:currency forKey:key];
NSInteger window = [SKANSnippet getCurrentSkanWindow];
switch (window) {
case 0:
key = isAdmon ? TOTAL_ADMON_REVENUE_P0 : TOTAL_REVENUE_P0 ;
break;
case 1:
key = isAdmon ? TOTAL_ADMON_REVENUE_P1 : TOTAL_REVENUE_P1 ;
break;
case 2:
key = isAdmon ? TOTAL_ADMON_REVENUE_P2 : TOTAL_REVENUE_P2 ;
break;
case -1:
key = nil;
return;
}
[SKANSnippet addToTotalRevenue:newRevenue withCurrency:currency forKey:key];
}
// Coarse value is being sent on requests and responses as an Int and being translated into the system defined coarse value upon API execution.
+ (NSString *)resolveCoarseValueFrom:(NSNumber *)value {
if(@available(iOS 16.1, *)) {
if (!value) {
return nil;
}
switch ([value integerValue]) {
case 0:
return SKAdNetworkCoarseConversionValueLow;
case 1:
return SKAdNetworkCoarseConversionValueMedium;
case 2:
return SKAdNetworkCoarseConversionValueHigh;
default:
return nil;
}
}
return nil;
}
+ (NSDictionary*)getTotalRevenue:(NSString *)revenueKey {
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
if (![userDefaults objectForKey:revenueKey]){
[userDefaults setObject:[NSDictionary dictionary] forKey:revenueKey];
}
return [userDefaults objectForKey:revenueKey];
}
+ (void)addToTotalRevenue:(NSNumber *)newRevenue withCurrency:(NSString *)currency forKey:(NSString *)revenueKey {
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
NSMutableDictionary *revenues = [[SKANSnippet getTotalRevenue:revenueKey] mutableCopy];
NSNumber *currentRevenue = @(0);
if ([revenues objectForKey:currency]) {
currentRevenue = [revenues objectForKey:currency];
}
currentRevenue = @([currentRevenue floatValue] + [newRevenue floatValue]);
[revenues setObject:currentRevenue forKey:currency];
[userDefaults setObject:revenues forKey:revenueKey];
}
@end
S2S Integration Update
Enhance server-to-server integration with SKAdNetwork metadata for implementation validation and troubleshooting (recommended for all implementations).
Metadata Structure
Data Retrieval
Use getSkanDetails method to extract metadata dictionary
and forward to server for appending as query parameters on Session and
Event endpoint API requests.
Critical: Metadata should be forwarded on every session and every event reported to Singular via Session Notification Endpoint and Event Notification Endpoint.
NSDictionary *skanMetadata = [SKANSnippet getSkanDetails];
// Forward skanMetadata to your server for S2S API enrichment
App Lifecycle Implementation
Integrate SKAdNetwork methods at appropriate app lifecycle points for complete attribution coverage with SKAN 4.0 features.
Implementation Examples
Implementation Notes:
- Uses asynchronous methods for conversion value updates to prevent blocking main thread
- All SKAN-related data collected in dictionary format before server transmission
- Follows Apple's privacy-first approach while enabling essential attribution tracking
- Revenue tracking includes monetary value and currency specification for accurate reporting
App First Launch
Registers app with SKAdNetwork for attribution tracking and sends initial session data to Singular's endpoint. Only runs on first app launch to establish attribution tracking.
[SKANSnippet registerAppForAdNetworkAttribution];
NSDictionary *skanValues = [SKANSnippet getSkanDetails];
[self sendSessionToServer:skanValues]; // to Singular launch EP
Session Management
Updates conversion values after each session and sends updated SKAN details to track user engagement.
[SKANSnippet updateConversionValuesAsync:^(NSNumber *fine, NSNumber *coarse, BOOL lock, NSError *error) {
if (error) {
NSLog(@"Conversion value update failed: %@", error);
} else {
NSLog(@"Values updated - Fine: %@, Coarse: %@, Lock: %d", fine, coarse, lock);
}
}];
NSDictionary *skanValues = [SKANSnippet getSkanDetails];
[self sendSessionToServer:skanValues];
Event Tracking
Handles non-revenue events by updating conversion values and sending event data to Singular's event endpoint.
[SKANSnippet updateConversionValuesAsync:@"event_name"
withCompletionHandler:^(NSNumber *fine, NSNumber *coarse, BOOL lock, NSError *error) {
if (error) {
NSLog(@"Event conversion update failed: %@", error);
} else {
NSLog(@"Event values updated - Fine: %@, Coarse: %@", fine, coarse);
}
}];
NSDictionary *skanValues = [SKANSnippet getSkanDetails];
[self sendEventToServer:skanValues eventName:@"event_name"];
Revenue Tracking
Manages revenue events by updating revenue amount with currency and associated conversion values, then sending to Singular's event endpoint for purchase-related activities.
[SKANSnippet updateRevenue:15.53 andCurrency:@"USD" isAdMonetization:NO];
[SKANSnippet updateConversionValuesAsync:@"revenue_event_name"
withCompletionHandler:^(NSNumber *fine, NSNumber *coarse, BOOL lock, NSError *error) {
if (error) {
NSLog(@"Revenue conversion update failed: %@", error);
} else {
NSLog(@"Revenue values updated - Fine: %@, Coarse: %@", fine, coarse);
}
}];
NSDictionary *skanValues = [SKANSnippet getSkanDetails];
[self sendEventToServer:skanValues eventName:@"revenue_event_name"];
Conversion Value API
Alternative server-side approach for reporting SKAdNetwork conversion values using REST API endpoint instead of client-side interface implementation.
API Overview
Implementation Methods
SKAdNetwork conversion values can be reported through two methods with identical data flow and reporting integrity.
- Direct SKAdNetwork Interface: Client-side implementation (see above)
- Server-Side Integration: Using Conversion Value API Endpoint
Conversion Value API Endpoint accepts identical parameters as client-side interface, ensuring consistent attribution tracking with flexibility to choose implementation best fitting technical architecture.
API Endpoint
HTTP Method and URL
GET https://sdk-api-v1.singular.net/api/v2/conversion_value
Required Parameters
API Key
| Parameter | Description |
|---|---|
a
|
Singular SDK Key from
Developer Tools.
Do not use reporting API Key. Example: sdkKey_afdadsf7asf56
|
Device Identifiers
| Parameter | Description |
|---|---|
idfa
|
Identifier for Advertisers (IDFA) for ad tracking and attribution.
Starting iOS 14.5, requires ATT framework opt-in. Omit if
unavailable (do not pass NULL or empty string). Example: DFC5A647-9043-4699-B2A5-76F03A97064B
|
idfv
|
Identifier for Vendors (IDFV) - required on all requests
regardless of ATT status. Unique per vendor/developer across
their app ecosystem. Example: 21DB6612-09B3-4ECC-84AC-B353B0AF1334
|
Device Parameters
| Parameter | Description |
|---|---|
p
|
Platform of app (must be "iOS" for this API). Example: iOS
|
v
|
OS Version of device at session time. Example: 16.1
|
Application Parameters
| Parameter | Description |
|---|---|
i
|
App Identifier (Bundle ID for iOS application, case-sensitive). Example: com.singular.app
|
app_v
|
Application Version. Example: 1.2.3
|
Event Parameters
| Parameter | Description |
|---|---|
n
|
Event name being tracked (max 32 ASCII characters). For sessions
use __SESSION__. For events, use same name and
casing sent to Singular via Event API.Example: sng_add_to_cart
|
Conversion Value Parameters
| Parameter | Description |
|---|---|
skan_current_conversion_valueiOS 15.4+ |
Latest SKAdNetwork conversion value at time of previous session/event
(0-63). Example: 7
|
p1_coarseiOS 16.1+ |
Latest SKAdNetwork coarse conversion value for postback_sequence
1 (0-2). Example: 0
|
p2_coarseiOS 16.1+ |
Latest SKAdNetwork coarse conversion value for postback_sequence
2 (0-2). Example: 1
|
Revenue Tracking Parameters
| Parameter | Description |
|---|---|
skan_total_revenue_by_currencyiOS 15.4+ |
Required for IAP or All Revenue models. Current aggregated
total of IAP revenue (excluding ad monetization), JSON URL-encoded
string. Example: %7B%22USD%22%3A9.99%7D
|
skan_total_admon_revenue_by_currencyiOS 15.4+ |
Required for Admon or All Revenue models. Current aggregated
total of ad monetization revenue, JSON URL-encoded string. Example: %7B%22USD%22%3A1.2%7D
|
Timestamp Parameters
| Parameter | Description |
|---|---|
skan_first_call_to_skadnetwork_timestampiOS 15.4+ |
Unix timestamp of first call to underlying SKAdNetwork API. Example: 1483228800
|
skan_last_call_to_skadnetwork_timestampiOS 15.4+ |
Unix timestamp of latest call to underlying SKAdNetwork API
at time of this session notification. Example: 1483228800
|
Request Examples
Sample Implementations
Code samples demonstrate core required parameters. When implementing, include all required parameters and validate correct values before production use.
import requests
params = {
'a': 'sdk_key_here',
'p': 'iOS',
'i': 'com.singular.app',
'v': '16.1',
'idfa': 'DFC5A647-9043-4699-B2A5-76F03A97064B',
'idfv': '21DB6612-09B3-4ECC-84AC-B353B0AF1334',
'n': '__SESSION__',
'app_v': '1.2.3',
'skan_current_conversion_value': 7,
'p1_coarse': 0,
'p2_coarse': 1,
'skan_total_revenue_by_currency': {"USD":9.99},
'skan_total_admon_revenue_by_currency': {"USD":1.2},
'skan_first_call_to_skadnetwork_timestamp': 1510090877,
'skan_last_call_to_skadnetwork_timestamp': 1510090877
}
response = requests.get('https://sdk-api-v1.singular.net/api/v2/conversion_value', params=params)
print(response.json())
curl -G 'https://sdk-api-v1.singular.net/api/v2/conversion_value' \
--data-urlencode 'a=sdk_key_here' \
--data-urlencode 'p=iOS' \
--data-urlencode 'i=com.singular.app' \
--data-urlencode 'v=16.1' \
--data-urlencode 'idfa=DFC5A647-9043-4699-B2A5-76F03A97064B' \
--data-urlencode 'idfv=21DB6612-09B3-4ECC-84AC-B353B0AF1334' \
--data-urlencode 'n=__SESSION__' \
--data-urlencode 'app_v=1.2.3' \
--data-urlencode 'skan_current_conversion_value=7' \
--data-urlencode 'p1_coarse=0' \
--data-urlencode 'p2_coarse=1' \
--data-urlencode 'skan_total_revenue_by_currency={"USD":9.99}' \
--data-urlencode 'skan_total_admon_revenue_by_currency={"USD":1.2}' \
--data-urlencode 'skan_first_call_to_skadnetwork_timestamp=1510040127' \
--data-urlencode 'skan_last_call_to_skadnetwork_timestamp=1510090877'
GET /api/v2/conversion_value
?a=sdk_key_here
&p=iOS
&i=com.singular.app
&v=16.1
&idfa=DFC5A647-9043-4699-B2A5-76F03A97064B
&idfv=21DB6612-09B3-4ECC-84AC-B353B0AF1334
&n=__SESSION__
&app_v=1.2.3
&skan_current_conversion_value=7
&p1_coarse=0
&p2_coarse=1
&skan_total_revenue_by_currency=%7B%22USD%22%3A9.99%7D
&skan_total_admon_revenue_by_currency=%7B%22USD%22%3A1.2%7D
&skan_first_call_to_skadnetwork_timestamp=1510090877
&skan_last_call_to_skadnetwork_timestamp=1510090877 HTTP/1.1
Host: sdk-api-v1.singular.net
Accept: application/json
Response Format
Successful Response
HTTP 200 - ok response without error or reason indicates request sent to queue for processing.
{
"conversion_value":1,
"skan_updated_coarse_value":0,
"postback_sequence_index":0,
"status":"ok"
}
Response Parameters
| Key | Description | Example |
|---|---|---|
conversion_value
|
New fine conversion value |
0-63
|
skan_updated_coarse_value
|
New coarse conversion value |
0-2
|
postback_sequence_index
|
SKAN postback measurement period (0=postback 1, 1=postback 2, 2=postback 3). Indicates which coarse value key to update |
0-2
|
status
|
Processing status |
ok
|
Possible Errors
- More than 24 hours past since last conversion update (28032 hours), update window closed
- Unknown platform error - Non iOS platform
- Conversion Management: Invalid parameter given
- Conversion Management: Conversion Model not found for app
- Invalid measurement period
- Conversion Management: Cannot find currency of owner