From apple-kit-skills
Measures ad effectiveness with privacy-preserving attribution using AdAttributionKit for iOS 17.4+ / Swift 6.3. Use when registering ad impressions, handling postbacks, updating conversion values, implementing re-engagement, or configuring publisher/advertiser apps.
How this skill is triggered — by the user, by Claude, or both
Slash command
/apple-kit-skills:adattributionkitThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Privacy-preserving ad attribution for iOS 17.4+ / Swift 6.3. AdAttributionKit
Privacy-preserving ad attribution for iOS 17.4+ / Swift 6.3. AdAttributionKit lets ad networks measure conversions (installs and re-engagements) without exposing user-level data. It supports the App Store and alternative marketplaces, and interoperates with SKAdNetwork.
Three roles exist in the attribution flow: the ad network (signs impressions, receives postbacks), the publisher app (displays ads), and the advertised app (the app being promoted).
AdAttributionKit preserves user privacy through several mechanisms:
In migration and interoperability reviews, explicitly state that the system evaluates AdAttributionKit and SKAdNetwork impressions together, only one impression wins per conversion, click-through beats view-through, and recency breaks ties within click-through impressions before falling back to the most recent view-through impression.
A publisher app displays ads from registered ad networks. Add each ad network's ID to the app's Info.plist so its impressions qualify for install validation.
<key>AdNetworkIdentifiers</key>
<array>
<string>example123.adattributionkit</string>
<string>another456.adattributionkit</string>
</array>
Ad network IDs must be lowercase. SKAdNetwork IDs (ending in .skadnetwork)
are also accepted -- the frameworks share IDs.
For click-through custom-rendered ads, place one UIEventAttributionView over
each tappable ad/control. It must cover the tappable area and stay above views
that would intercept touches before handleTap() succeeds.
import UIKit
let attributionView = UIEventAttributionView()
attributionView.frame = adContentView.bounds
attributionView.isUserInteractionEnabled = true
adContentView.addSubview(attributionView)
The advertised app is the app someone installs or re-engages with after seeing an ad. It must call a conversion value update at least once to begin the postback conversion window.
Add AttributionCopyEndpoint under the top-level AdAttributionKit Info.plist
dictionary so the device sends a copy of the winning postback to your server:
<key>AdAttributionKit</key>
<dict>
<key>AttributionCopyEndpoint</key>
<string>https://example.com</string>
</dict>
The system derives the well-known endpoint from the registrable domain in the URL, ignoring subdomains:
https://example.com/.well-known/appattribution/report-attribution/
Configure your server to accept HTTPS POST requests at that path. The domain must have a valid SSL certificate.
Add a second key in the same AdAttributionKit dictionary to also receive
copies of winning re-engagement postbacks:
<key>AdAttributionKit</key>
<dict>
<key>AttributionCopyEndpoint</key>
<string>https://example.com</string>
<key>OptInForReengagementPostbackCopies</key>
<true/>
</dict>
Call a conversion value update as early as possible after first launch to begin the conversion window:
import AdAttributionKit
func applicationDidFinishLaunching() async {
do {
try await Postback.updateConversionValue(0, lockPostback: false)
} catch {
print("Failed to set initial conversion value: \(error)")
}
}
Ad networks create signed impressions using JWS (JSON Web Signature). The
publisher app uses AppImpression to register and handle those impressions.
import AdAttributionKit
let impression = try await AppImpression(compactJWS: signedJWSString)
The JWS contains the ad network ID, advertised item ID, publisher item ID, source identifier, timestamp, and optional re-engagement eligibility flag. See references/adattributionkit-patterns.md for JWS generation details.
guard AppImpression.isSupported else {
// Fall back to alternative ad display
return
}
Record a view impression when the ad content has been displayed and dismissed:
func handleAdViewed(impression: AppImpression) async {
do {
try await impression.handleView()
} catch {
print("Failed to record view-through impression: \(error)")
}
}
For long-lived ad views, use beginView() and endView() to track view
duration:
try await impression.beginView()
// ... ad remains visible ...
try await impression.endView()
Respond to ad taps by calling handleTap() within 15 minutes of creating the
AppImpression; otherwise request a fresh impression. If the advertised app is
not installed, the system opens its App Store or marketplace page. If installed,
the system launches it directly.
func handleAdTapped(impression: AppImpression) async {
do {
try await impression.handleTap()
} catch {
print("Failed to record click-through impression: \(error)")
}
}
A UIEventAttributionView must overlay the ad for handleTap() to succeed.
Pass the impression to StoreKit overlay or product view controller APIs. StoreKit automatically records view-through impressions after 2 seconds of display and click-through impressions on tap.
import StoreKit
let config = SKOverlay.AppConfiguration(appIdentifier: "1234567890",
position: .bottom)
config.appImpression = impression
Postbacks are attribution reports the device sends to ad networks (and optionally to the advertised app developer) after a conversion event.
Three windows produce up to three postbacks for winning attributions:
| Window | Duration | Postback delay |
|---|---|---|
| 1st | Days 0-2 | 24-48 hours |
| 2nd | Days 3-7 | 24-144 hours |
| 3rd | Days 8-35 | 24-144 hours |
Tier 0 postbacks only produce the first postback. Nonwinning attributions produce only one postback.
| Event | Time limit |
|---|---|
| View-through to install | 24 hours (configurable up to 7 days) |
| Click-through to install | 30 days (configurable down to 1 day) |
| Install to first update | 60 days |
| Re-engagement to first update | 2 days |
Lock the postback to finalize a conversion value before the window ends and receive the postback sooner:
try await Postback.updateConversionValue(
42,
coarseConversionValue: .high,
lockPostback: true
)
After locking, the system ignores further updates in that conversion window.
| Field | Tier 0 | Tier 1 | Tier 2 | Tier 3 |
|---|---|---|---|---|
source-identifier digits | 2 | 2 | 2-4 | 2-4 |
conversion-value (fine) | -- | -- | 1st only | 1st only |
coarse-conversion-value | -- | 1st only | 2nd/3rd | 2nd/3rd |
publisher-item-identifier | -- | -- | -- | Yes |
country-code | -- | -- | -- | Conditional |
Fine values are integers from 0...63 (6 bits). They are available only in the first postback and only at Tier 2 or higher:
try await Postback.updateConversionValue(
35,
coarseConversionValue: .medium,
lockPostback: false
)
Three levels for lower tiers and second/third postbacks:
// CoarseConversionValue cases: .low, .medium, .high
try await Postback.updateConversionValue(
10,
coarseConversionValue: .high,
lockPostback: false
)
Separate conversion values for install vs. re-engagement postbacks. In server
JSON, use "conversion-type": "re-engagement" with the hyphen; Swift APIs use
.reengagement without it.
let installUpdate = PostbackUpdate(
fineConversionValue: 20,
lockPostback: false,
conversionTypes: [.install]
)
try await Postback.updateConversionValue(installUpdate)
let reengagementUpdate = PostbackUpdate(
fineConversionValue: 12,
lockPostback: false,
conversionTypes: [.reengagement]
)
try await Postback.updateConversionValue(reengagementUpdate)
Use conversion tags to selectively update specific postbacks when overlapping conversion windows exist:
let update = PostbackUpdate(
fineConversionValue: 15,
lockPostback: false,
conversionTag: savedConversionTag,
conversionTypes: [.reengagement]
)
try await Postback.updateConversionValue(update)
The system delivers the conversion tag through the re-engagement URL's
AdAttributionKitReengagementOpen query parameter.
Re-engagement tracks users who already have the advertised app installed and interact with an ad to return to it.
Set eligible-for-re-engagement to true in the JWS payload when generating
the impression.
Pass a universal link that the system opens in the advertised app:
let reengagementURL = URL(string: "https://example.com/promo/summer")!
try await impression.handleTap(reengagementURL: reengagementURL)
The system appends AdAttributionKitReengagementOpen as a query parameter. The
advertised app checks for this parameter to detect AdAttributionKit-driven
opens:
func handleUniversalLink(_ url: URL) {
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
let isReengagement = components?.queryItems?.contains(where: {
$0.name == Postback.reengagementOpenURLParameter
}) ?? false
if isReengagement {
// AdAttributionKit opened this app via a re-engagement ad
}
}
AdAttributionKitReengagementOpen parameter is always present on the
URL, even when the system does not create a postback.// DON'T -- never updating the conversion value
func appDidLaunch() {
// No conversion value update; postback window never starts
}
// DO -- update conversion value on first launch
func appDidLaunch() async {
try? await Postback.updateConversionValue(0, lockPostback: false)
}
<!-- DON'T -->
<string>Example123.AdAttributionKit</string>
<!-- DO -->
<string>example123.adattributionkit</string>
// DON'T -- tap without a current attribution view tap or fresh impression
try await staleImpression.handleTap()
// Throws if the tap cannot be validated or the impression expired
// DO -- ensure UIEventAttributionView covers the ad and the impression is fresh
let attributionView = UIEventAttributionView()
attributionView.frame = adView.bounds
adView.addSubview(attributionView)
// Then handle the tap within 15 minutes after creating the AppImpression
try await impression.handleTap()
// DON'T
try? await impression.handleTap()
// DO -- handle specific errors
do {
try await impression.handleTap()
} catch let error as AdAttributionKitError {
switch error {
case .impressionExpired:
// Impression expired or is stale for click-through handling
refreshAdImpression()
case .missingAttributionView:
// UIEventAttributionView not present
break
default:
print("Attribution error: \(error)")
}
}
// DON'T -- silently dropping the request
// The device retries up to 9 times over 9 days on HTTP 500
// DO -- respond with 200 OK immediately
// Server handler:
func handlePostback(request: Request) -> Response {
// Process asynchronously, respond immediately
Task { await processPostback(request.body) }
return Response(status: .ok)
}
AdNetworkIdentifiers
(lowercase)kidUIEventAttributionView overlays each tappable click-through ad/controlAppImpression is no older than 15 minutes at handleTap()updateConversionValue on first launchpostback-identifierAppImpression.isSupported checked before attempting impression APIsnpx claudepluginhub dpearson2699/swift-ios-skills --plugin all-ios-skillsAnalyzes Apple Ads (formerly Apple Search Ads) campaign structure, bid health, Custom Product Pages, and attribution for mobile app advertisers.
Generates a complete, platform-specific ad conversion tracking setup (Meta Pixel/CAPI, TikTok Events API, Google Ads, GTM) from a single chat message. Auto-detects industry, maps standard events, and outputs a developer-ready implementation doc.
Interprets paid media dashboards, including attribution models, platform reporting quirks, ROAS vs LTV, multi-platform reconciliation, and incrementality testing. Useful when scaling, killing, or rebudgeting campaigns based on platform metrics.