SKAdNetwork 4 Available: For the latest SKAdNetwork version, see SKAdNetwork 4 Implementation Guide.
SKAdNetwork 3.0 S2S Implementation Guide
Implement Apple's SKAdNetwork 3.0 framework for privacy-compliant iOS attribution using server-to-server integration to measure app install campaign performance without compromising user identifiers.
Overview
What is SKAdNetwork?
SKAdNetwork is Apple's privacy-preserving attribution framework enabling measurement of app install ad campaign conversion rates on iOS without requiring user-level identifiers.
The framework processes attribution through App Store servers, disconnecting attribution information from user identifiers and temporal data before sending to ad networks.
Apple documentation: SKAdNetwork Framework Reference
How SKAdNetwork Works
Attribution process occurs entirely through Apple's infrastructure, ensuring user privacy while enabling campaign performance measurement.
Attribution Flow:
- Ad Click: User clicks ad containing SKAdNetwork signature with network, publisher, and campaign ID
- App Store Open: Device stores attribution data and opens App Store for install
- App Launch: User installs and launches app for first time
- Registration: App registers with SKAdNetwork framework to signal successful install
- Conversion Value: App optionally updates conversion value (0-63) representing post-install activity
- Timer Window: Device waits 24+ hours after first launch or last conversion value update
- Postback: App Store sends attribution postback to ad network with campaign ID, conversion value, and cryptographic signature
Privacy by Design:
- Postback contains no device or user identifiers
- Minimum 24-hour delay prevents temporal correlation
- App never knows which ad user clicked
- Network never knows which specific user installed
Capabilities & Limitations
SKAdNetwork Provides:
- Last-Click Attribution: Works without user consent or ATT opt-in
- Campaign Breakdown: Source, campaign, and publisher granularity
- Conversion Values: 64 discrete values (0-63) for post-install measurement
- Fraud Prevention: Cryptographic signatures validate attribution authenticity
Current Limitations:
- No User-Level Data: Cannot track individual user journeys
- No View-Through: Click-based attribution only
- Limited Conversion Values: Single 6-bit value (0-63) per install
- Limited Granularity: Maximum 100 campaign IDs, no ad group or creative breakdown
- No Long Cohorts: Limited timeframe for conversion measurement
- Fraud Exposure: Conversion value itself not signed, postbacks can be duplicated
Singular Resources
Singular provides comprehensive resources for SKAdNetwork implementation and optimization.
- SKAN: Practical Standard for SKAdNetwork Implementation
- SKAdNetwork 101: What It Means for You
- SKAdNetwork Code: GitHub Repository for Networks, Publishers, Advertisers
- How to Test SKAdNetwork: Step-by-Step Instructions
- Singular Announces First-to-Market IDFA Replacement Support
- Advanced Measurement Using SKAdNetwork: Unlocking ROAS
- Secure SKAdNetwork 2.0: Seamless Trust Establishment Setup
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 |
| 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 |
| 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 |
S2S Integration Architecture
Implementation Components
Server-to-server SKAdNetwork integration consists of two main implementation areas.
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
2. S2S Integration Update (Optional):
- Enriches Singular S2S events and sessions with SKAdNetwork metadata
- Enables implementation validation and troubleshooting
- Provides conversion value timestamps for debugging
Client-Side Implementation
Implement SKAdNetwork framework registration and conversion value management using Singular's native iOS code samples for optimal campaign measurement.
Implementation Responsibilities
Core Functionality
Client-side code handles two critical functions for SKAdNetwork measurement.
- 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 (first call timestamp, last call timestamp, current value)
SKAdNetwork Interface
Method Definitions
Interface includes methods for framework registration, session tracking, event tracking, and revenue measurement.
// SKANSnippet.h
#import <Foundation/Foundation.h>
@interface SKANSnippet : NSObject
// Register for SKAdNetwork attribution.
// Call this method as soon as possible once app is launched
+ (void)registerAppForAdNetworkAttribution;
// Track retention and cohorts by calling after each session.
// Reports session details and updates conversion value if needed.
// Conversion value updates only if new value greater than previous.
// Optional callback runs once conversion value updated.
+ (void)updateConversionValueAsync:(void(^)(int, NSError*))handler;
// Track conversion events by calling after each event.
// Reports event details and updates conversion value if needed.
// Optional callback runs once conversion value updated.
+ (void)updateConversionValueAsync:(NSString*)eventName
withCompletionHandler:(void(^)(int, NSError*))handler;
// Track revenue by calling before each revenue event.
// Updates total revenue for next conversion value calculation.
// Note:
// 1. Call before 'updateConversionValueAsync' to ensure revenue included
// 2. Avoid calling on event retries to prevent double-counting
+ (void)updateRevenue:(double)amount andCurrency:(NSString*)currency;
// Gets current conversion value (nil if none set)
+ (NSNumber *)getConversionValue;
@end
Registration Method
registerAppForAdNetworkAttribution registers app with SKAdNetwork
framework, starting 24-hour attribution timer.
Timer Behavior:
- Device sends install notification 0-24 hours after timer expires
- Updating conversion value with higher value resets timer for new 24-hour interval
- Apple reference: updateConversionValue Documentation
Conversion Value Methods
Methods calculate and update conversion values based on post-install activity and configured conversion model.
Supported Activities:
- Sessions: Critical for retention measurement
- Conversion Events: Critical for post-install event measurement
- Revenue Events: Critical for revenue measurement
Interface Implementation
Complete Implementation Code
Full implementation handles registration, conversion management, and Singular API communication.
// SKANSnippet.m
#import "SKANSnippet.h"
#import <StoreKit/SKAdNetwork.h>
#define SESSION_EVENT_NAME @"__SESSION__"
#define SINGULAR_API_URL @"https://sdk-api-v1.singular.net/api/v1/conversion_value"
// Keys for UserDefaults storage
#define CONVERSION_VALUE_KEY @"skan_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_KEY @"skan_total_revenue_by_currency"
#define SECONDS_PER_DAY 86400
static NSLock *lockObject;
@implementation SKANSnippet
+ (void)registerAppForAdNetworkAttribution {
if ([SKANSnippet getFirstSkanCallTimestamp] != 0) {
return;
}
if (@available(iOS 11.3, *)) {
[SKAdNetwork registerAppForAdNetworkAttribution];
[SKANSnippet setFirstSkanCallTimestamp];
[SKANSnippet setLastSkanCallTimestamp];
}
}
+ (void)updateConversionValueAsync:(void(^)(int, NSError*))handler {
[SKANSnippet updateConversionValueAsync:SESSION_EVENT_NAME withCompletionHandler:handler];
}
+ (void)updateConversionValueAsync:(NSString*)eventName withCompletionHandler:(void(^)(int, NSError*))handler {
if (@available(iOS 14, *)) {
if ([SKANSnippet isSkanUpdateWindowOver]) {
return;
}
[SKANSnippet getConversionValueFromServer:eventName withCompletionHandler:handler];
}
}
+ (void)updateRevenue:(double)amount andCurrency:(NSString*)currency {
if (@available(iOS 14, *)) {
// Update total revenues
if (amount != 0 && currency) {
NSMutableDictionary* revenues = [[SKANSnippet getTotalRevenue] mutableCopy];
NSNumber* currentRevenue = @(0);
if ([revenues objectForKey:currency]) {
currentRevenue = [revenues objectForKey:currency];
}
currentRevenue = @([currentRevenue floatValue] + amount);
[revenues setObject:currentRevenue forKey:currency];
[SKANSnippet setTotalRevenue:revenues];
}
}
}
+ (NSNumber *)getConversionValue {
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
if (![userDefaults objectForKey:CONVERSION_VALUE_KEY]) {
return nil;
}
return @([userDefaults integerForKey:CONVERSION_VALUE_KEY]);
}
+ (void)getConversionValueFromServer:(NSString*)eventName withCompletionHandler:(void(^)(int, NSError*))handler {
if (!lockObject) {
lockObject = [NSLock new];
}
// 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];
NSURLComponents *components = [NSURLComponents componentsWithString:SINGULAR_API_URL];
NSString* bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
components.queryItems = @[
[NSURLQueryItem queryItemWithName:@"a" value:@"YOUR_SDK_KEY"],
[NSURLQueryItem queryItemWithName:@"i" value:bundleIdentifier],
[NSURLQueryItem queryItemWithName:@"app_v" value:@"YOUR_APP_VERSION"],
[NSURLQueryItem queryItemWithName:@"n" value:eventName],
[NSURLQueryItem queryItemWithName:@"p" value:@"iOS"],
[NSURLQueryItem queryItemWithName:@"idfv" value:@"YOUR_IDFV"],
[NSURLQueryItem queryItemWithName:@"idfa" value:@"YOUR_IDFA"],
[NSURLQueryItem queryItemWithName:@"conversion_value" value:[[SKANSnippet getConversionValue] stringValue]],
[NSURLQueryItem queryItemWithName:@"total_revenue_by_currency" value:[SKANSnippet dictionaryToJsonString:[SKANSnippet getTotalRevenue]]],
[NSURLQueryItem queryItemWithName:@"first_call_to_skadnetwork_timestamp" value:[NSString stringWithFormat:@"%ld", [SKANSnippet getFirstSkanCallTimestamp]]],
[NSURLQueryItem queryItemWithName:@"last_call_to_skadnetwork_timestamp" value:[NSString stringWithFormat:@"%ld", [SKANSnippet getLastSkanCallTimestamp]]],
];
[[[NSURLSession sharedSession] dataTaskWithURL:components.URL
completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
[lockObject unlock];
if (handler) {
handler(-1, error);
}
return;
}
NSDictionary* parsedResponse = [SKANSnippet jsonDataToDictionary:data];
if (!parsedResponse) {
[lockObject unlock];
if (handler) {
handler(-1, [NSError errorWithDomain:bundleIdentifier
code:0
userInfo:@{NSLocalizedDescriptionKey:@"Failed parsing server response"}]);
}
return;
}
NSNumber *conversionValue = [parsedResponse objectForKey:@"conversion_value"];
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(-1, [NSError errorWithDomain:bundleIdentifier
code:0
userInfo:@{NSLocalizedDescriptionKey:reason}]);
}
}
return;
}
NSNumber* currentValue = [SKANSnippet getConversionValue];
if ([conversionValue intValue] <= [currentValue intValue]) {
[lockObject unlock];
return;
}
[SKANSnippet setConversionValue:[conversionValue intValue]];
[SKANSnippet setLastSkanCallTimestamp];
if (![SKANSnippet getFirstSkanCallTimestamp]) {
[SKANSnippet setFirstSkanCallTimestamp];
}
[lockObject unlock];
if (handler) {
handler([conversionValue intValue], error);
}
}] resume];
});
}
+ (BOOL)isSkanUpdateWindowOver {
NSInteger timeDiff = [SKANSnippet getCurrentUnixTimestamp] - [SKANSnippet getLastSkanCallTimestamp];
return SECONDS_PER_DAY <= timeDiff;
}
@end
Utility Methods
Metadata Management
Utility methods manage SKAdNetwork metadata storage in UserDefaults, critical for conversion value calculation.
Critical Implementation: These utilities maintain metadata required for accurate conversion value calculation. Do not modify unless necessary.
+ (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];
}
+ (NSDictionary*)getTotalRevenue {
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
if (![userDefaults objectForKey:TOTAL_REVENUE_BY_CURRENCY_KEY]) {
return [NSDictionary new];
}
return [userDefaults objectForKey:TOTAL_REVENUE_BY_CURRENCY_KEY];
}
+ (NSInteger)getCurrentUnixTimestamp {
return [[NSDate date]timeIntervalSince1970];
}
+ (void)setConversionValue:(int)value {
if (@available(iOS 14.0, *)) {
if (value <= [[SKANSnippet getConversionValue] intValue]) {
return;
}
[SKAdNetwork updateConversionValue:value];
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setInteger:value forKey:CONVERSION_VALUE_KEY];
[userDefaults synchronize];
}
}
+ (void)setFirstSkanCallTimestamp {
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setInteger:[SKANSnippet getCurrentUnixTimestamp]
forKey:FIRST_SKAN_CALL_TIMESTAMP];
[userDefaults synchronize];
}
+ (void)setLastSkanCallTimestamp {
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setInteger:[SKANSnippet getCurrentUnixTimestamp]
forKey:LAST_SKAN_CALL_TIMESTAMP];
[userDefaults synchronize];
}
+ (void)setTotalRevenue:(NSDictionary *)values {
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setObject:values forKey:TOTAL_REVENUE_BY_CURRENCY_KEY];
[userDefaults synchronize];
}
+ (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];
}
+ (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;
}
@end
S2S Integration Update
Optionally enhance server-to-server integration with SKAdNetwork metadata for implementation validation and troubleshooting.
Mandatory Updates
Conversion Value Response
Once conversion model activated, S2S endpoints return
conversion_value integer field containing next value for
client-side update.
Response Handling: Parse conversion_value
from S2S endpoint responses and apply to SKAdNetwork framework via
client-side code.
Optional Enhancements
Metadata Parameters
Enrich S2S session and event requests with SKAdNetwork metadata to validate integration and troubleshoot implementation issues.
| Parameter | Description |
|---|---|
skan_conversion_value
|
Latest conversion value at time of request |
skan_first_call_timestamp
|
Unix timestamp of first call to SKAdNetwork API |
skan_last_call_timestamp
|
Unix timestamp of latest call to SKAdNetwork API |
Metadata Extraction Code
Extract SKAdNetwork metadata values using utility methods for S2S transmission.
NSDictionary *skanMetadata = @{
@"skan_conversion_value":
[[SKANSnippet getConversionValue] stringValue],
@"skan_first_call_timestamp":
[NSString stringWithFormat:@"%ld", [SKANSnippet getFirstSkanCallTimestamp]],
@"skan_last_call_timestamp":
[NSString stringWithFormat:@"%ld", [SKANSnippet getLastSkanCallTimestamp]]
};
// Forward skanMetadata to your server for S2S API enrichment
Forward parameters to server-side and append to Singular S2S API requests. Complete parameter documentation: S2S API Reference
Integration Flow
Complete SKAdNetwork flow for S2S customers from client-side conversion management to postback processing.
Flow Stages
End-to-End Process
- 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 24+ hours from last update, 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 marketing parameters based on 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.
App Lifecycle Implementation
Integrate SKAdNetwork methods at appropriate app lifecycle points for complete attribution coverage.
Implementation Example
Method Placement
// On app launch (in applicationDidFinishLaunching)
[SKANSnippet registerAppForAdNetworkAttribution];
// After each session handled
[SKANSnippet updateConversionValueAsync:^(int value, NSError *error) {
if (error) {
NSLog(@"Conversion value update failed: %@", error);
} else {
NSLog(@"Conversion value updated to: %d", value);
}
}];
// After handling non-revenue events
[SKANSnippet updateConversionValueAsync:@"event_name"
withCompletionHandler:^(int value, NSError *error) {
if (error) {
NSLog(@"Event conversion value update failed: %@", error);
} else {
NSLog(@"Event conversion value updated to: %d", value);
}
}];
// After handling revenue events
[SKANSnippet updateRevenue:15.53 andCurrency:@"USD"];
[SKANSnippet updateConversionValueAsync:@"revenue_event_name"
withCompletionHandler:^(int value, NSError *error) {
if (error) {
NSLog(@"Revenue conversion value update failed: %@", error);
} else {
NSLog(@"Revenue conversion value updated to: %d", value);
}
}];
Testing Implementation
Validate SKAdNetwork implementation using Singular's test endpoint before production deployment.
Test Environment
Test Endpoint Configuration
Replace production conversion management endpoint with test endpoint to simulate app flow.
Test Endpoint URL:
https://skadnetwork-testing.singular.net/api/v1/conversion_value
- No API key required for test endpoint
- Returns conversion values 0-2 sequentially
- Returns empty string after third call
Test Procedure
Validation Steps
-
Update Endpoint: Replace
SINGULAR_API_URLconstant with test endpoint URL - Initialize Conversion Value: Ensure default conversion value initialized to 0
- Generate Events: Trigger 3 different events within app
-
Verify Updates: Confirm
updateConversionValueAsynccalled after each event - Log Values: Log returned conversion values to verify correct progression (0 → 1 → 2)
- Confirm Completion: After third call, verify empty response and final value retention
Expected Behavior:
- First event: Receives conversion value 0
- Second event: Receives conversion value 1
- Third event: Receives conversion value 2
- Fourth+ events: Empty response, value 2 persists
Code Changelog
Track code sample updates and critical fixes applied over time.
Version History
| Date | Changes |
|---|---|
| October 1, 2020 |
|
| September 23, 2020 |
|
| September 15, 2020 |
|