Apple Search Ads is the only advertising platform that places your app at the top of App Store search results. Unlike web-based ad platforms where you drop a pixel on a page, Apple Search Ads attribution is deeply tied to Apple's privacy frameworks -- AdServices, SKAdNetwork, and App Tracking Transparency (ATT). A misconfigured implementation does not just lose data; it means your attribution tokens never generate, your SKAdNetwork postbacks never register, and your campaign optimization runs on zero signal.
Why Proper Implementation Matters
Attribution in Apple's Privacy-First World
Apple Search Ads operates under stricter privacy constraints than any web ad platform:
- No IDFA by default: Since iOS 14.5, users must opt in via ATT. Typical opt-in rates are 15-30%.
- AdServices framework replaces the deprecated iAd framework and provides deterministic attribution without requiring user consent
- SKAdNetwork provides privacy-preserving install attribution with delayed, aggregated postbacks
- No real-time conversion data: SKAdNetwork postbacks arrive 24-48 hours after install with limited conversion value bits
If you rely solely on ATT consent for attribution, you will lose measurement on 70-85% of installs. A proper implementation layers AdServices attribution (deterministic, no consent needed) with SKAdNetwork (privacy-preserving aggregated data) to maximize coverage.
Campaign Optimization Consequences
Without accurate attribution:
- Search Match campaigns cannot learn which queries drive installs
- Cost-per-acquisition (CPA) goals have no conversion signal to optimize against
- Keyword bid adjustments rely on incomplete data, leading to overbidding on poor keywords
- Creative Sets and Custom Product Pages cannot be evaluated for performance
- Audience refinement (age, gender, location) lacks conversion feedback
Revenue Attribution
For subscription apps, the real value of an install happens days or weeks later. Apple Search Ads supports postback conversion values that encode post-install behavior (trial start, subscription purchase, retention milestones). Getting the conversion value schema right determines whether you can measure true ROAS or only install volume.
Pre-Implementation Planning
Access and Permissions
Apple Search Ads Account:
- Sign in at
searchads.apple.comwith your Apple ID - Request Account Admin or Campaign Manager role
- For agencies: Accept invitation from the app owner's account
- Verify your Apple Developer Program membership is active
Required Access:
| Platform | Role Needed | Purpose |
|---|---|---|
| Apple Search Ads | Campaign Manager+ | Create campaigns, view attribution data |
| App Store Connect | Admin or Developer | Access app metadata, configure custom product pages |
| Xcode Project | Developer | Add AdServices framework, configure SKAdNetwork |
| Apple Developer Portal | Account Holder/Admin | Register SKAdNetwork IDs, manage entitlements |
Mobile Measurement Partner (MMP) Decision:
- If using an MMP (AppsFlyer, Adjust, Branch, Singular, Kochava): The MMP SDK handles AdServices token retrieval and SKAdNetwork postback registration
- If building in-house: You need to implement AdServices token retrieval and server-side attribution API calls yourself
- Recommendation: Use an MMP unless you have a dedicated mobile measurement engineering team
Conversion Value Schema Design
SKAdNetwork provides 6 bits (values 0-63) to encode post-install conversion information. Design your schema before writing any code:
Subscription App Example:
| Value Range | Meaning | Timeframe |
|---|---|---|
| 0 | Install only, no engagement | Day 0 |
| 1-10 | App opened X times | Days 0-3 |
| 11-20 | Completed onboarding | Days 0-3 |
| 21-30 | Started free trial | Days 0-7 |
| 31-40 | Trial active, high engagement | Days 3-7 |
| 41-50 | Converted to paid subscription | Days 0-14 |
| 51-60 | Tier 2+ subscription or annual plan | Days 0-14 |
| 61-63 | Reserved for future use | -- |
E-commerce App Example:
| Value Range | Meaning |
|---|---|
| 0 | Install, no activity |
| 1-15 | Browsed X product categories |
| 16-30 | Added items to cart (value bucket) |
| 31-45 | Completed first purchase (revenue bucket) |
| 46-63 | Repeat purchaser (LTV bucket) |
Design the schema to answer: "What is the highest-value action this user took within the conversion window?"
Implementation Walkthrough
Step 1: Add AdServices Framework to Your Xcode Project
AdServices provides deterministic attribution for Apple Search Ads without requiring ATT consent.
import AdServices
class AttributionManager {
/// Retrieves the Apple Search Ads attribution token on first launch.
/// Call this in application(_:didFinishLaunchingWithOptions:) or on first app open.
func fetchAttributionToken() {
// AdServices is available on iOS 14.3+
if #available(iOS 14.3, *) {
do {
let token = try AAAttribution.attributionToken()
// Send this token to your server for attribution lookup
sendTokenToServer(token: token)
} catch {
// Token generation failed -- user may not have come from an ad
print("AdServices attribution token error: \(error.localizedDescription)")
}
}
}
private func sendTokenToServer(token: String) {
guard let url = URL(string: "https://your-server.com/api/attribution") else { return }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body: [String: Any] = [
"attribution_token": token,
"app_bundle_id": Bundle.main.bundleIdentifier ?? "",
"timestamp": ISO8601DateFormatter().string(from: Date())
]
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
print("Failed to send attribution token: \(error)")
}
}.resume()
}
}
Step 2: Server-Side Attribution API Call
Your server receives the token from the app and calls Apple's Attribution API to get campaign details.
import requests
APPLE_ATTRIBUTION_URL = "https://api-adservices.apple.com/api/v1/"
def get_attribution(token: str) -> dict:
"""
Exchange an AdServices token for attribution data.
Returns campaign name, ad group, keyword, and other details.
"""
headers = {
"Content-Type": "text/plain"
}
response = requests.post(
APPLE_ATTRIBUTION_URL,
data=token,
headers=headers
)
if response.status_code == 200:
data = response.json()
# data contains:
# - attribution: true/false (was this an Apple Search Ads install?)
# - campaignId, adGroupId, keywordId
# - campaignName, adGroupName, keyword
# - clickDate, conversionType
return data
elif response.status_code == 404:
# Not an Apple Search Ads install
return {"attribution": False}
else:
raise Exception(f"Attribution API error: {response.status_code}")
Important timing notes:
- Call the Attribution API within 24 hours of token generation for best results
- The API may return a 404 if the install was not from Apple Search Ads
- Tokens are single-use; do not retry with the same token after a successful call
Step 3: Configure SKAdNetwork
SKAdNetwork provides aggregated, privacy-preserving attribution data even when users have not consented to tracking.
Add SKAdNetwork IDs to Info.plist:
<!-- Info.plist -->
<key>SKAdNetworkItems</key>
<array>
<!-- Apple Search Ads Network ID -->
<dict>
<key>SKAdNetworkIdentifier</key>
<string>YDX93A7ASS.skadnetwork</string>
</dict>
<!-- Add MMP network IDs if using a measurement partner -->
<!-- AppsFlyer -->
<dict>
<key>SKAdNetworkIdentifier</key>
<string>PBHKH8B9YZ.skadnetwork</string>
</dict>
</array>
Register Conversion Values:
import StoreKit
class ConversionValueManager {
/// Update the SKAdNetwork conversion value based on user behavior.
/// Call this whenever the user completes a valuable action.
func updateConversionValue(for event: AppEvent) {
let newValue = calculateConversionValue(event: event)
if #available(iOS 16.1, *) {
// SKAdNetwork 4.0 -- supports coarse and fine conversion values
SKAdNetwork.updatePostbackConversionValue(newValue, coarseValue: .high) { error in
if let error = error {
print("SKAdNetwork update error: \(error)")
}
}
} else if #available(iOS 15.4, *) {
SKAdNetwork.updatePostbackConversionValue(newValue) { error in
if let error = error {
print("SKAdNetwork update error: \(error)")
}
}
} else {
// iOS 14.5 - 15.3
SKAdNetwork.registerAppForAdNetworkAttribution()
SKAdNetwork.updateConversionValue(newValue)
}
}
private func calculateConversionValue(event: AppEvent) -> Int {
// Map your app events to conversion value schema
switch event {
case .appOpen:
return max(currentValue, 1)
case .onboardingComplete:
return max(currentValue, 15)
case .trialStarted:
return max(currentValue, 25)
case .subscriptionPurchased(let tier):
return max(currentValue, tier == .annual ? 55 : 45)
default:
return currentValue
}
}
}
Key rules for conversion values:
- Values can only increase, never decrease (SKAdNetwork ignores lower values)
- The conversion window is 24 hours by default; each update resets the timer (up to 63 days with SKAdNetwork 4.0)
- Postbacks arrive 24-48 hours after the conversion window closes
- Design values so higher numbers always mean higher user value
Step 4: Configure App Tracking Transparency (Optional but Recommended)
While not required for AdServices or SKAdNetwork, ATT consent enables richer attribution data.
import AppTrackingTransparency
func requestTrackingPermission() {
// Request after onboarding, not immediately on first launch
ATTrackingManager.requestTrackingAuthorization { status in
switch status {
case .authorized:
// Full attribution data available
// If using MMP, notify it of consent
print("Tracking authorized")
case .denied, .restricted:
// AdServices + SKAdNetwork still work
print("Tracking denied -- using privacy-preserving attribution")
case .notDetermined:
print("Tracking status not yet determined")
@unknown default:
break
}
}
}
Best practices for ATT prompt:
- Show a pre-prompt screen explaining why tracking helps (better ads, free app)
- Request permission after the user has experienced app value, not on first launch
- Never gate app functionality behind tracking consent
- Customize the
NSUserTrackingUsageDescriptionstring in Info.plist
Step 5: Configure Custom Product Pages (CPPs)
Custom Product Pages let you create up to 35 variants of your App Store listing, each with different screenshots, app previews, and promotional text. Link these to specific ad groups for higher relevance.
- In App Store Connect, navigate to your app > Custom Product Pages
- Create a new CPP with screenshots and copy matching your ad group's theme
- Submit for review (CPPs require Apple approval)
- In Apple Search Ads, navigate to Ad Group > Ads
- Create a new ad and select the approved CPP
- The CPP URL format is:
https://apps.apple.com/app/id123456789?ppid=CPP_ID
Verification and QA
AdServices Token Validation
On-Device Testing:
- Install a debug build on a physical device (simulators do not generate real tokens)
- Search for your app in the App Store and tap the ad (if running test campaigns)
- Install and launch the app
- Check your server logs for the attribution token submission
- Verify the Attribution API response contains
"attribution": truewith campaign details
Common Issues:
- Token generation throws error: Ensure AdServices.framework is linked in Xcode Build Phases
- Attribution API returns 404: The install was organic, not from an ad
- Empty campaign data: The token may have expired (24-hour window)
SKAdNetwork Postback Validation
Testing SKAdNetwork is inherently difficult because postbacks are delayed and arrive at the ad network's registered URL, not your server.
What you can verify:
- Confirm
SKAdNetworkItemsin Info.plist includes Apple's network ID - Use
printstatements or analytics events to verifyupdatePostbackConversionValueis called - In Apple Search Ads, check the "Install (SK)" column in campaign reports (appears 24-48 hours after install)
- If using an MMP, check the MMP dashboard for SKAdNetwork install and conversion value data
Campaign Dashboard Verification
In Apple Search Ads at searchads.apple.com:
- Navigate to Campaigns > select a campaign > Ad Groups
- Check the "Installs" column for attributed installs
- Verify "CPA" (cost per acquisition) is calculating correctly
- Check "Conversion Rate" (taps to installs) is within expected range (typically 40-60% for search ads)
- Under Attribution, verify both AdServices and SKAdNetwork data streams are reporting
Debug Tools
| Tool | Purpose |
|---|---|
| Xcode Console | View AdServices token generation and SKAdNetwork update calls |
| Charles Proxy / Proxyman | Intercept attribution token POST to your server |
| Apple Search Ads Dashboard | View attributed installs, CPA, and conversion rates |
| MMP Dashboard (if applicable) | Unified view of AdServices + SKAdNetwork attribution |
| TestFlight | Test attribution flow with beta builds (limited -- real attribution requires App Store installs) |
Deployment Artifacts
- AdServices integration code: Token retrieval in app, server-side API endpoint
- SKAdNetwork configuration: Info.plist network IDs, conversion value schema documentation
- Conversion value mapping: Spreadsheet mapping app events to SKAdNetwork values 0-63
- ATT prompt implementation: Pre-prompt screen copy,
NSUserTrackingUsageDescriptionstring - Custom Product Pages: CPP IDs, associated ad groups, and screenshot variants
- MMP SDK configuration (if applicable): SDK initialization code, server-to-server callback URLs
- Campaign structure: Campaign > Ad Group > Keyword hierarchy with max CPA targets
Linked Runbooks
- Install or Embed Tag or SDK -- AdServices framework integration and MMP SDK setup
- Event Tracking -- Post-install event tracking and conversion value updates
- Data Layer Setup -- Structuring in-app events for attribution
- Cross-Domain Tracking -- Deep linking from ads to specific app content
- Server-Side vs Client-Side -- AdServices server-side API vs. on-device attribution
Change Log and Owners
- Document who manages the Apple Search Ads account and who has App Store Connect access
- Track conversion value schema changes with version numbers and effective dates
- Record MMP configuration changes and SDK version updates
- Maintain a log of Custom Product Page approvals and ad group associations
- Review quarterly: attribution data completeness, conversion value distribution, CPA trends