adattributionkit
dpearson2699/swift-ios-skills
Privacy-preserving ad attribution for iOS 17.4+ measuring conversions without exposing user data.
What is adattributionkit?
AdAttributionKit measures ad effectiveness (installs and re-engagements) while preserving user privacy through crowd anonymity tiers and delayed postbacks. Use it when you need to register ad impressions, handle attribution postbacks, update conversion values, or replace SKAdNetwork for ad measurement on iOS 17.4+.
- Register signed ad impressions (JWS) from ad networks in publisher apps
- Record view-through and click-through impressions with optional duration tracking
- Handle attribution postbacks across three conversion windows (0-2, 3-7, 8-35 days)
- Update conversion values to trigger postback windows in advertiser apps
- Support re-engagement attribution with separate postback copies
- Interoperate with SKAdNetwork using shared ad network IDs
How to install adattributionkit
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill adattributionkit- iOS 17.4 or later
- Swift 6.3 or later
- Ad network ID registered in publisher app Info.plist (AdNetworkIdentifiers)
- Valid HTTPS endpoint for advertiser apps receiving postback copies
- UIEventAttributionView overlay for click-through custom-rendered ads
How to use adattributionkit
- 1.Add ad network IDs to publisher app Info.plist under AdNetworkIdentifiers key
- 2.Place UIEventAttributionView over tappable ad areas in publisher app
- 3.Create AppImpression from signed JWS: AppImpression(compactJWS:)
- 4.Call impression.handleView() for view-through or impression.handleTap() for clicks
- 5.In advertiser app, call Postback.updateConversionValue() on first launch to begin conversion window
- 6.Configure AttributionCopyEndpoint in advertiser app Info.plist to receive postback copies
- 7.Update conversion values as needed; postbacks trigger after 24-48 hours (first window) or 24-144 hours (subsequent windows)
Use cases
- Ad networks measuring install conversions without user-level data exposure
- Publisher apps displaying ads from multiple registered ad networks
- Advertiser apps receiving winning postback copies at a configured endpoint
- Migrating from SKAdNetwork to AdAttributionKit while maintaining interoperability
- Implementing re-engagement campaigns with separate attribution tracking
- Ad network engineers building attribution systems
- iOS app developers displaying third-party ads
- Advertiser app teams tracking post-install conversions
- Mobile marketing teams migrating from SKAdNetwork
adattributionkit FAQ
It uses crowd anonymity tiers (Tier 0-3) to limit postback data granularity, delays postbacks 24-48 hours (first window) or 24-144 hours (later windows), excludes user-level identifiers, and uses hierarchical source IDs (2-4 digits) based on crowd size.
Add each ad network's ID to Info.plist under AdNetworkIdentifiers (e.g., 'example123.adattributionkit'). For click-through custom ads, overlay a UIEventAttributionView covering the tappable area.
Add AttributionCopyEndpoint under the AdAttributionKit Info.plist dictionary with your HTTPS endpoint URL. The system will POST winning postbacks to https://yourdomain/.well-known/appattribution/report-attribution/.
The device evaluates both together; only one impression wins per conversion. Click-through beats view-through, recency breaks ties within click-through, then falls back to most recent view-through.
Call it as early as possible after first launch (in applicationDidFinishLaunching) with an initial value to begin the conversion window. Update it later as user actions warrant higher conversion values.
Full instructions (SKILL.md)
Source of truth, from dpearson2699/swift-ios-skills.
name: adattributionkit description: "Measure ad effectiveness with privacy-preserving attribution using AdAttributionKit. Use when registering ad impressions, handling attribution postbacks, updating conversion values, implementing re-engagement attribution, configuring publisher or advertiser apps, or replacing SKAdNetwork with AdAttributionKit for ad measurement."
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).
Contents
- Overview and Privacy Model
- Publisher App Setup
- Advertiser App Setup
- Impressions
- Postbacks
- Conversion Values
- Re-engagement
- Common Mistakes
- Review Checklist
- References
Overview and Privacy Model
AdAttributionKit preserves user privacy through several mechanisms:
- Crowd anonymity tiers -- the device limits postback data granularity based on the crowd size associated with the ad, ranging from Tier 0 (minimal data) to Tier 3 (most data including publisher ID and country code).
- Time-delayed postbacks -- postbacks are sent 24-48 hours after conversion window close (first window) or 24-144 hours (second/third windows).
- No user-level identifiers -- postbacks contain aggregate source identifiers and conversion values, not device or user IDs.
- Hierarchical source identifiers -- 2, 3, or 4-digit source IDs where the number of digits returned depends on the crowd anonymity tier.
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.
Publisher App Setup
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.
Add ad network identifiers
<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.
Display a UIEventAttributionView
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)
Advertiser App Setup
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.
Opt in to receive winning postback copies
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.
Opt in for re-engagement postback copies
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>
Update conversion value on first launch
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)")
}
}
Impressions
Ad networks create signed impressions using JWS (JSON Web Signature). The
publisher app uses AppImpression to register and handle those impressions.
Create an impression from a JWS
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.
Check device support
guard AppImpression.isSupported else {
// Fall back to alternative ad display
return
}
View-through impressions
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()
Click-through impressions
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.
StoreKit-rendered ads
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
Postbacks are attribution reports the device sends to ad networks (and optionally to the advertised app developer) after a conversion event.
Conversion windows
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.
Time windows for events
| 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 conversion values early
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.
Postback data by tier
| 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 |
Conversion Values
Fine-grained values
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
)
Coarse values
Three levels for lower tiers and second/third postbacks:
// CoarseConversionValue cases: .low, .medium, .high
try await Postback.updateConversionValue(
10,
coarseConversionValue: .high,
lockPostback: false
)
Update by conversion type (iOS 18+)
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)
Conversion tags (iOS 18.4+)
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
Re-engagement tracks users who already have the advertised app installed and interact with an ad to return to it.
Mark impressions as re-engagement eligible
Set eligible-for-re-engagement to true in the JWS payload when generating
the impression.
Handle re-engagement taps with a URL
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
}
}
Re-engagement limits
- Only click-through interactions create re-engagement postbacks (not view-through).
- The device enforces monthly per-app and yearly per-device re-engagement limits.
- The
AdAttributionKitReengagementOpenparameter is always present on the URL, even when the system does not create a postback.
Common Mistakes
Forgetting to update conversion value on launch
// 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)
}
Using uppercase ad network IDs
<!-- DON'T -->
<string>Example123.AdAttributionKit</string>
<!-- DO -->
<string>example123.adattributionkit</string>
Calling handleTap without a current UIEventAttributionView tap
// 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()
Ignoring handleTap errors
// 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)")
}
}
Not responding to postback requests
// 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)
}
Review Checklist
- Publisher app includes all ad network IDs in
AdNetworkIdentifiers(lowercase) - Ad network IDs match between publisher app's Info.plist and JWS
kid -
UIEventAttributionViewoverlays each tappable click-through ad/control - Click-through
AppImpressionis no older than 15 minutes athandleTap() - Advertised app calls
updateConversionValueon first launch - Server endpoint at well-known path accepts HTTPS POST with valid SSL
- Postback verification uses correct Apple public key for environment
- Duplicate postbacks filtered by
postback-identifier - Server responds with HTTP 200 to postback requests
- Re-engagement URL is a registered universal link for the advertised app
- Conversion value strategy accounts for all three conversion windows
-
AppImpression.isSupportedchecked before attempting impression APIs
References
- references/adattributionkit-patterns.md -- postback verification, server handling, testing, SKAdNetwork migration, alternative marketplaces, attribution rules configuration
- Apple: AdAttributionKit
- Apple: Presenting ads in your app
- Apple: Receiving ad attributions and postbacks
- Apple: Verifying a postback
- Apple: SKAdNetwork interoperability
Related skills
More from dpearson2699/swift-ios-skills and the wider catalog.

alarmkit
Schedule system alarms and countdown timers with Lock Screen, Dynamic Island, and Apple Watch integration for iOS 26+.

app-clips
Build lightweight iOS App Clips with invocation routing, handoff, and App Store Connect experiences.

app-intents
Expose iOS app actions and data to Siri, Shortcuts, Spotlight, widgets, Control Center, and Apple Intelligence.

app-store-optimization
Optimize App Store product pages for search visibility and conversion. Use for App Store Optimization (ASO), keyword research, app name/subtitle/keyword-field strategy, conversion-focused descriptions and promotional text, screenshot captions and ordering, Custom Product Pages with assigned search keywords, In-App Events, Product Page Optimization tests, localized metadata, ratings/review strategy, and in-app review prompt timing with RequestReviewAction or AppStore.requestReview. Also use when routing ASO vs App Store review, privacy/ATT, or StoreKit implementation boundaries.

app-store-review
Prevent App Store rejections with guidance on review guidelines, privacy manifests, IAP rules, and compliance checklists.

apple-on-device-ai
Integrate on-device AI using Foundation Models, Core ML, and open-source LLM runtimes on Apple Silicon.