Symptom Library
requestAttributionDetailsreturningnilor timing out after 3 seconds- SKAdNetwork postbacks not received by your endpoint
- Conversion values stuck at
0or not updating to expected schema values - Apple Ads Attribution API returning HTTP
403or401errors - MMP (AppsFlyer, Adjust, Branch) install counts diverging from Apple dashboard by more than 20%
- App Analytics showing zero Search Ads installs despite active campaigns
SKAdNetwork Attribution Debugging
Common failure points:
- Missing
SKAdNetworkItemsin Info.plist -- Apple Search Ads usescstr6suwn9.skadnetwork. Verify:
<key>SKAdNetworkItems</key>
<array>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>cstr6suwn9.skadnetwork</string>
</dict>
</array>
- Postback endpoint unreachable -- Your server must respond within 5 seconds with
200. Test with:
curl -X POST https://your-endpoint.com/skadnetwork \
-H "Content-Type: application/json" \
-d '{"version":"4.0","ad-network-id":"cstr6suwn9.skadnetwork","campaign-id":42}'
- Timer window expired -- SKAdNetwork 4.0 uses three postback windows: 0-2 days, 3-7 days, and 8-35 days. Conversion values set after the window closes are silently dropped.
Swift Diagnostic Code
Attribution Token Retrieval
Use AdServices (iOS 14.3+) to retrieve the attribution token. This replaces the deprecated iAd framework:
import AdServices
func fetchAttributionToken() {
do {
let token = try AAAttribution.attributionToken()
print("[SearchAds] Token retrieved: \(token.prefix(20))...")
var request = URLRequest(url: URL(string: "https://api-adservices.apple.com/api/v1/")!)
request.httpMethod = "POST"
request.setValue("text/plain", forHTTPHeaderField: "Content-Type")
request.httpBody = token.data(using: .utf8)
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error { print("[SearchAds] Network error: \(error.localizedDescription)"); return }
let status = (response as? HTTPURLResponse)?.statusCode ?? 0
switch status {
case 200:
guard let data = data,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return }
print("[SearchAds] Attributed: \(json["attribution"] ?? false), Org: \(json["orgId"] ?? "nil")")
case 400: print("[SearchAds] ERROR 400: Invalid token. Regenerate.")
case 401: print("[SearchAds] ERROR 401: Token expired/used. Tokens are single-use.")
case 403: print("[SearchAds] ERROR 403: Not entitled. Check Xcode Signing & Capabilities.")
case 404: print("[SearchAds] 404: Organic install (no attribution data).")
case 500: print("[SearchAds] ERROR 500: Apple server error. Retry with backoff.")
default: print("[SearchAds] Unexpected status: \(status)")
}
}.resume()
} catch {
let code = (error as NSError).code
// code 1: networkError (no internet), code 2: internalError (framework bug)
print("[SearchAds] AAAttribution error (code \(code)): \(error.localizedDescription)")
}
}
SKAdNetwork Conversion Value Update
import StoreKit
func updateConversionValue(fineValue: Int, coarseValue: SKAdNetwork.CoarseConversionValue) {
if #available(iOS 16.1, *) {
SKAdNetwork.updatePostbackConversionValue(fineValue, coarseValue: coarseValue) { error in
if let error = error {
let code = (error as NSError).code
// code 1: invalid value (out of 0-63), code 2: window expired, code 3: no initial value set
print("[SKAdNetwork] Update failed (code \(code)): \(error.localizedDescription)")
} else {
print("[SKAdNetwork] Updated to fine=\(fineValue), coarse=\(coarseValue)")
}
}
} else {
SKAdNetwork.updatePostbackConversionValue(fineValue) { error in
if let error = error { print("[SKAdNetwork] Legacy update failed: \(error.localizedDescription)") }
}
}
}
Debug Logging on Launch
Add to AppDelegate.didFinishLaunchingWithOptions for attribution diagnostics:
#if DEBUG
print("[SearchAds] iOS \(UIDevice.current.systemVersion)")
if #available(iOS 14.3, *) { fetchAttributionToken() }
let skItems = Bundle.main.object(forInfoDictionaryKey: "SKAdNetworkItems") as? [[String: String]]
print("[SearchAds] SKAdNetwork IDs: \(skItems?.count ?? 0), Apple registered: \(skItems?.contains { $0["SKAdNetworkIdentifier"] == "cstr6suwn9.skadnetwork" } ?? false)")
#endif
Apple Ads Attribution API Response Codes
| HTTP Code | Error | Fix |
|---|---|---|
200 |
Success | Parse the attribution and orgId fields |
400 |
Invalid attribution token format | Regenerate the token via AAAttribution.attributionToken() |
401 |
Unauthorized -- invalid or expired token | Tokens are single-use and expire after 24 hours; request a new one |
403 |
App not entitled for Search Ads | Enable the Search Ads entitlement in Xcode under Signing & Capabilities |
404 |
No attribution data found | User may have installed organically; handle this as a non-attributed install |
500 |
Apple server error | Retry with exponential backoff (max 3 retries) |
Framework Error Codes
AAAttributionError.networkError (code 1)
Device cannot reach api-adservices.apple.com. Caused by no internet on launch or VPN/firewall blocking Apple's domain.
- Fix: Cache the failure and retry on next app launch or when reachability changes. Do not block the main thread.
AAAttributionError.internalError (code 2)
Internal framework failure, not caused by your code. Rare, typically an iOS bug.
- Fix: Log the iOS version and device model. File a Feedback Assistant report. No client-side workaround.
SKAdNetworkError.missingPostbackConversionValue (code 3)
Called updatePostbackConversionValue without setting an initial value after install.
- Fix: Call
updatePostbackConversionValueon first launch after install, even with value0, to start the SKAdNetwork timer.
App Not Entitled Error (Xcode Fix)
If AAAttribution.attributionToken() throws or the API returns 403:
- In Xcode, select the app target > Signing & Capabilities > + Capability > search "AdServices".
- If missing, regenerate your Provisioning Profile in the Apple Developer portal with
com.apple.developer.ad-servicesenabled for your App ID. - Clean build folder (Cmd+Shift+K) and rebuild.
App Store Connect Integration Issues
- Confirm the same Apple ID owns both the App Store Connect account and the Search Ads organization.
- Verify the SKAdNetwork conversion value schema is configured in App Store Connect > App Analytics > Frameworks > SKAdNetwork.
- Allow 48 hours after first install for data to appear (Apple aggregates and applies privacy thresholds).
- Minimum 10 installs/day from Search Ads required; below this, Apple suppresses data for privacy.
MMP Discrepancy Causes
Attribution Methodology Differences
- AppsFlyer uses deterministic matching on the Apple attribution token, falling back to probabilistic fingerprinting. This can inflate counts by 5-15% compared to Apple's deterministic-only dashboard.
- Adjust relies on the AdServices token as primary signal with device-level click matching as fallback. Adjust may attribute to Search Ads even if a later organic link click exists, depending on your attribution window.
- Branch uses server-to-server integration and attributes differently when deep links are involved. A user who clicks a Search Ads result, then later installs via a Branch deep link, may be attributed to Branch instead.
Reporting Lag
Apple Search Ads data updates on a 24-hour cycle (midnight PST cutoff). MMPs process installs in near-real-time. Always compare completed date ranges with a 48-hour buffer to avoid mismatches.
Privacy Threshold Suppression
Apple suppresses data below privacy thresholds:
- Fewer than 10 installs/day per campaign: counts may be rounded or hidden.
- Demographic breakdowns suppressed below 100 installs per segment.
- LAT-on (Limit Ad Tracking) users are excluded from attribution entirely and appear as organic.
The MMP-vs-Apple gap widens for low-volume campaigns and privacy-conscious user bases.
Keyword Match Type Conflicts
- Exact vs. Broad overlap: Exact Match takes priority. Use negative keywords in the Broad Match ad group to prevent cannibalization.
- Search Match conflicts: When enabled alongside manual keywords, Apple may serve ads for queries you already bid on, inflating costs. Check the Search Terms report weekly.
- Budget delivery: "Standard" delivery may underspend if bids are below the suggested range. Switch to "Accelerated" temporarily to diagnose.
Campaign Budget Delivery Issues
- Check the Suggested Bid Range -- bids below the minimum yield zero impressions.
- Review Audience Refinements -- overly narrow age/gender/location reduces inventory.
- Verify the correct storefront (country/region). A US campaign will not serve in UK App Store.
- Apple may overspend by up to 20% on high-traffic days and compensate over the billing period.
Search Ads API Troubleshooting
| Error Code | Fix |
|---|---|
UNAUTHORIZED |
Refresh OAuth token. If refresh fails, re-authenticate through the Search Ads OAuth flow. |
FORBIDDEN |
API user needs "Campaign Manager" role. Update in Search Ads UI > Users. |
INVALID_FIELD |
Check: dailyBudgetAmount >= 0.01, cpaGoal > 0, countries use ISO 3166-1 alpha-2. |
ENTITY_NOT_FOUND |
Verify entity ID belongs to the orgId in your request header. |
LIMIT_EXCEEDED |
Max 1,000 entities per batch request. Split into multiple calls. |
Report API Timeout Handling
- Reports spanning 90+ days may take 30-60 seconds. Set HTTP client timeout to at least 120 seconds.
- If a report times out, reduce the date range or filter to a single campaign.
- For recurring reports, use incremental date ranges (fetch yesterday only) instead of the full window each time.
- The API returns
429 Too Many Requestsabove 200 calls/hour per organization. Implement a request queue with 20-second intervals for bulk operations.
OAuth 2.0 Client Secret Rotation
Apple Search Ads API client secrets have a 180-day maximum lifetime. When expired, all calls fail with UNAUTHORIZED.
- Generate a new secret in the Search Ads UI under Settings > API.
- The secret is a JWT signed with your private key. Set
expto no more than 180 days fromiat. - Deploy the new secret before the old one expires (set a reminder 2 weeks prior). The old secret is immediately invalid after rotation with no grace period.
Escalation & Communication
- Apple Search Ads support: File tickets via Campaign Manager with Org ID and campaign ID.
- Apple Developer support for ADServices and SKAdNetwork framework bugs.
- MMP technical support for SDK-level attribution discrepancies.
Preventive Maintenance
- Monthly: Audit SKAdNetwork conversion value schema against actual in-app events.
- Quarterly: Compare Apple dashboard installs to MMP-reported installs.
- After iOS updates: Test
AAAttribution.attributionToken()-- behavior changes with major releases. - Weekly: Review Search Terms report and prune negative keywords.
- After SDK changes: Verify
SKAdNetworkItemsin Info.plist.