Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.
71
89%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Advisory
Suggest reviewing before use
Extended patterns for postback verification, server-side handling, testing, SKAdNetwork migration, alternative marketplaces, and attribution rules configuration.
The device signs postbacks with Apple's private key. Verify the JWS signature before counting any conversion.
Apple uses different keys depending on the environment:
| Key ID | Environment |
|---|---|
apple-cas-identifier/0 | Production |
apple-development-identifier/0 | Development (end-to-end flows) |
apple-development-identifier/1 | Development (developer settings) |
import CryptoKit
import Foundation
enum PostbackVerificationError: Error {
case unknownKeyID
case invalidPublicKey
case invalidJWS
case signatureVerificationFailed
}
struct PostbackVerifier {
static func verify(jwsString: String) throws -> [String: Any] {
let parts = jwsString.split(separator: ".").map(String.init)
guard parts.count == 3 else {
throw PostbackVerificationError.invalidJWS
}
let headerData = try base64URLDecode(parts[0])
let payloadData = try base64URLDecode(parts[1])
let signatureData = try base64URLDecode(parts[2])
guard let header = try JSONSerialization.jsonObject(
with: headerData
) as? [String: String],
let keyID = header["kid"] else {
throw PostbackVerificationError.invalidJWS
}
let publicKey = try getPublicKey(forKeyID: keyID)
let signedData = Data("\(parts[0]).\(parts[1])".utf8)
let signature = try P256.Signing.ECDSASignature(
rawRepresentation: signatureData
)
guard publicKey.isValidSignature(signature, for: signedData) else {
throw PostbackVerificationError.signatureVerificationFailed
}
guard let payload = try JSONSerialization.jsonObject(
with: payloadData
) as? [String: Any] else {
throw PostbackVerificationError.invalidJWS
}
return payload
}
static func getPublicKey(
forKeyID keyID: String
) throws -> P256.Signing.PublicKey {
let base64Key: String
switch keyID {
case "apple-cas-identifier/0":
base64Key = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWdp8GPcGqmhgzEFj9Z2nSpQVddayaPe4FMzqM9wib1+aHaaIzoHoLN9zW4K8y4SPykE3YVK3sVqW6Af0lfx3gg=="
case "apple-development-identifier/0":
base64Key = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAELeEDzpJEP+/qRSE5hJVC1p1J0ssUnQGMzBBbvnACBok8OVGGLgxL0myrKiy6lvRtSlLRsWit87i+vftD8AEqeQ=="
case "apple-development-identifier/1":
base64Key = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8YzdO7eM97s/IJ25kdW5CZ3A14USE5IJ5Ha/vhWaxI6UBI1ZxCEvjrKxVluVGe6qWwF1BDFq+QHqKfH5u+wxHQ=="
default:
throw PostbackVerificationError.unknownKeyID
}
guard let keyData = Data(base64Encoded: base64Key) else {
throw PostbackVerificationError.invalidPublicKey
}
return try P256.Signing.PublicKey(derRepresentation: keyData)
}
private static func base64URLDecode(_ string: String) throws -> Data {
var base64 = string
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
while base64.count % 4 != 0 {
base64.append("=")
}
guard let data = Data(base64Encoded: base64) else {
throw PostbackVerificationError.invalidJWS
}
return data
}
}In production, always use the apple-cas-identifier/0 key. Discard any
postback that fails signature verification -- do not count it as a conversion.
The device sends an HTTPS POST with JSON:
{
"jws-string": "<JWS compact serialization>",
"conversion-value": 24,
"ad-interaction-type": "click",
"country-code": "US"
}The conversion-value, ad-interaction-type, and country-code fields are
outside the JWS and present only at higher crowd anonymity tiers.
Decoded from the JWS jws-string:
| Field | Type | Description |
|---|---|---|
postback-identifier | String | Unique ID for deduplication |
ad-network-identifier | String | Ad network that signed the impression |
source-identifier | String | 2-4 digit hierarchical source ID |
advertised-item-identifier | Integer | App Store item ID of the advertised app |
publisher-item-identifier | Integer | App Store item ID of the publisher app (Tier 3) |
marketplace-identifier | String | Bundle ID of the marketplace (if not App Store) |
impression-type | String | Always "app-impression" |
did-win | Boolean | Whether this postback won attribution |
postback-sequence-index | Integer | 0, 1, or 2 (conversion window index) |
conversion-type | String | "download", "redownload", or "re-engagement" |
Use postback-identifier to deduplicate. Discard any postback with a
postback-identifier already processed.
Respond with HTTP 200 immediately. On HTTP 500, the device retries up to 9 times over a maximum of 9 days. Process the postback asynchronously.
Ad networks generate signed JWS impressions on their servers and deliver them to the publisher app.
{
"alg": "ES256",
"kid": "example.adattributionkit"
}Use ES256 encryption. The kid is the registered ad network ID.
{
"impression-identifier": "7aa9f8cc-5689-4c02-b963-22ca22136015",
"publisher-item-identifier": 525463029,
"impression-type": "app-impression",
"ad-network-identifier": "example.adattributionkit",
"source-identifier": 5239,
"timestamp": 1679790422446,
"advertised-item-identifier": 1108187390,
"eligible-for-re-engagement": true
}| Field | Required | Notes |
|---|---|---|
impression-identifier | Yes | UUID generated per impression |
publisher-item-identifier | Yes | Set to 0 for development impressions |
impression-type | Yes | Always "app-impression" |
ad-network-identifier | Yes | Must match kid in header |
source-identifier | Yes | 2-4 digit hierarchical ID |
timestamp | Yes | Milliseconds since 1970 |
advertised-item-identifier | Yes | App Store item ID |
eligible-for-re-engagement | No | true to enable re-engagement |
Sign with the private key generated during ad network registration using ES256 (ECDSA with P-256 and SHA-256). The compact JWS format is:
BASE64URL(header) . BASE64URL(payload) . BASE64URL(signature)Generate the ECDSA key pair using OpenSSL:
# Private key
openssl ecparam -name prime256v1 -genkey -noout \
-out company_adattributionkit_private_key.pem
# Public key (share with Apple during registration)
openssl ec -in company_adattributionkit_private_key.pem \
-pubout -out company_adattributionkit_public_key.pemDeveloper Mode reduces conversion windows from days to minutes and postback delays from hours to 5-10 minutes.
| Window | Normal duration | Developer Mode |
|---|---|---|
| 1st | Days 0-2 | 0-3 minutes |
| 2nd | Days 3-7 | 3-6 minutes |
| 3rd | Days 8-35 | 6-9 minutes |
| Delay | 24-48 hours | 5-10 minutes |
Use publisher-item-identifier: 0 in the JWS payload to create development
impressions. The system prioritizes production impressions over development
impressions if both exist.
With Developer Mode enabled, configure an HTTP proxy on the device (Settings > Wi-Fi > network > HTTP Proxy > Manual) to intercept AdAttributionKit postbacks.
apple-development-identifier/0 as the key ID.For testing without a publisher app, use the on-device development postbacks tool.
Tap "Transmit Development Postbacks" to send eligible postbacks immediately instead of waiting for automatic delivery.
kid in JWS header: apple-development-identifier/1ad-network-identifier: development.adattributionkitadvertised-item-identifier: 0 for Xcode-installed apps; actual ID for
App Store / marketplace installs.AdAttributionKit is the successor to SKAdNetwork. Both frameworks coexist and the system evaluates impressions from both when determining attribution winners.
| Feature | SKAdNetwork | AdAttributionKit |
|---|---|---|
| Marketplace support | App Store only | App Store + alt |
| Re-engagement | No | Yes (iOS 18+) |
| Property list key | SKAdNetworkItems | AdNetworkIdentifiers |
| Postback copy key | NSAdvertisingAttributionReportEndpoint | AttributionCopyEndpoint |
| Conversion value API | SKAdNetwork.updatePostbackConversionValue | Postback.updateConversionValue |
| Framework import | StoreKit | AdAttributionKit |
| Conversion tags | No | Yes (iOS 18.4+) |
| Attribution rules config | No | Yes (iOS 26+) |
If supporting both frameworks during migration, call both update APIs:
import AdAttributionKit
import StoreKit
func updateConversion(value: Int, coarse: CoarseConversionValue) async {
// Update AdAttributionKit
do {
try await Postback.updateConversionValue(
value,
coarseConversionValue: coarse,
lockPostback: false
)
} catch {
print("AdAttributionKit update failed: \(error)")
}
// Update SKAdNetwork
// Note: SKAdNetwork bridges to AdAttributionKit automatically,
// but calling both ensures coverage if only one framework has
// active postbacks.
do {
try await SKAdNetwork.updatePostbackConversionValue(
value,
coarseValue: .high,
lockWindow: false
)
} catch {
print("SKAdNetwork update failed: \(error)")
}
}When an app calls the SKAdNetwork update API, SKAdNetwork automatically bridges the conversion values to AdAttributionKit. Calling both APIs explicitly is still recommended for clarity and coverage.
Ad network IDs ending in .adattributionkit and .skadnetwork are compatible
across both frameworks. An ad network registered with SKAdNetwork can reuse its
existing ID with AdAttributionKit.
Include ad network IDs for both frameworks in Info.plist:
<!-- SKAdNetwork (legacy) -->
<key>SKAdNetworkItems</key>
<array>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>example123.skadnetwork</string>
</dict>
</array>
<!-- AdAttributionKit -->
<key>AdNetworkIdentifiers</key>
<array>
<string>example123.adattributionkit</string>
</array>AdAttributionKit supports alternative app marketplaces in addition to the App
Store. The marketplace-identifier field in postbacks contains the bundle ID
of the marketplace from which the conversion originated.
marketplace-identifier is com.apple.AppStore.marketplace-identifier is omitted from Tier 0 postbacks.No additional client-side setup is required. The system automatically includes the marketplace identifier based on where the app was installed from.
iOS 26+ supports configurable attribution windows and cooldown periods through Info.plist.
Control how long impressions remain eligible for attribution:
<key>AdAttributionKitConfigurations</key>
<dict>
<key>AttributionWindows</key>
<dict>
<!-- Global settings for all ad networks -->
<key>global</key>
<dict>
<key>install</key>
<dict>
<key>view</key>
<integer>3</integer> <!-- 1-7 days, default 1 -->
<key>click</key>
<integer>14</integer> <!-- 1-30 days, default 30 -->
</dict>
</dict>
<!-- Per-network override -->
<key>example.adattributionkit</key>
<dict>
<key>install</key>
<dict>
<key>click</key>
<integer>7</integer>
<key>ignoreInteractionType</key>
<string>view</string>
</dict>
</dict>
</dict>
</dict>Precedence: ad network config > global config > system default.
The ignoreInteractionType key is only valid in per-ad-network configurations,
not in the global section. Set it to "view" or "click" to ignore that
interaction type from the specified ad network during attribution.
Control the minimum time between conversions:
<key>AdAttributionKitConfigurations</key>
<dict>
<key>AttributionCooldown</key>
<dict>
<!-- Hours after install before accepting new conversions -->
<key>install-cooldown-hours</key>
<integer>24</integer> <!-- 0-720 hours -->
<!-- Hours after re-engagement before accepting new conversions -->
<key>re-engagement-cooldown-hours</key>
<integer>12</integer> <!-- 0-720 hours -->
</dict>
</dict>When multiple re-engagement conversions occur close together, conversion tags (iOS 18.4+) let the advertised app update specific postbacks independently.
The system delivers the conversion tag through the re-engagement URL:
func handleReengagementURL(_ url: URL) {
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
if let tagItem = components?.queryItems?.first(where: {
$0.name == Postback.reengagementOpenURLParameter
}) {
let conversionTag = tagItem.value
// Persist the tag alongside an internal conversion identifier
saveConversionTag(conversionTag)
}
}func updateSpecificConversion(tag: String, value: Int) async {
let update = PostbackUpdate(
fineConversionValue: value,
lockPostback: false,
conversionTag: tag,
conversionTypes: [.reengagement]
)
do {
try await Postback.updateConversionValue(update)
} catch {
print("Failed to update tagged conversion: \(error)")
}
}If no conversion tag is specified, AdAttributionKit updates the most recent conversion (pre-iOS 18.4 behavior).
| Error | Cause |
|---|---|
.impressionExpired | Impression older than its time window |
.missingAttributionView | handleTap() called without UIEventAttributionView |
.invalidImpressionJWSComponents | JWS string is not valid compact JWS format |
.invalidImpressionJWSHeader | JWS header missing required fields or wrong algorithm |
.invalidImpressionJWSPayload | JWS payload missing required fields |
.invalidImpressionJWSSignature | Signature verification failed |
.invalidConversionTag | Conversion tag format is invalid |
.conversionTagNotSupported | Conversion tag used on unsupported OS version |
.unknown | Unrecoverable internal error |
func safeHandleImpression(_ jwsString: String) async {
do {
let impression = try await AppImpression(compactJWS: jwsString)
try await impression.handleView()
} catch let error as AdAttributionKitError {
switch error {
case .invalidImpressionJWSComponents,
.invalidImpressionJWSHeader,
.invalidImpressionJWSPayload,
.invalidImpressionJWSSignature:
// Log and request a new impression from the ad network
reportInvalidImpression(jwsString, error: error)
case .impressionExpired:
// Request a fresh impression
refreshImpression()
case .missingAttributionView:
// Only relevant for handleTap, not handleView
break
default:
print("AdAttributionKit error: \(error)")
}
} catch {
print("Unexpected error: \(error)")
}
}skills
accessorysetupkit
references
activitykit
references
adattributionkit
references
alarmkit
references
app-clips
app-intents
references
app-store-optimization
app-store-review
apple-on-device-ai
appmigrationkit
references
audioaccessorykit
references
authentication
references
avkit
references
background-processing
references
browserenginekit
references
callkit
references
carplay
references
cloudkit
references
contacts-framework
references
core-bluetooth
references
core-data
core-motion
references
core-nfc
references
coreml
references
cryptokit
references
cryptotokenkit
references
debugging-instruments
device-integrity
references
dockkit
references
energykit
references
eventkit
references
financekit
references
focus-engine
gamekit
references
healthkit
references
homekit
references
ios-accessibility
ios-localization
ios-networking
ios-simulator
references
mapkit
metrickit
references
musickit
references
natural-language
references
paperkit
references
passkit
references
pdfkit
references
pencilkit
references
permissionkit
references
photokit
push-notifications
realitykit
references
relevancekit
references
scenekit
references
sensorkit
references
speech-recognition
spritekit
references
storekit
swift-api-design-guidelines
swift-architecture
swift-charts
references
swift-codable
swift-concurrency
swift-formatstyle
swift-language
swift-security
references
swift-testing
swiftdata
swiftlint
swiftui-animation
swiftui-gestures
references
swiftui-layout-components
swiftui-liquid-glass
references
swiftui-patterns
swiftui-performance
swiftui-uikit-interop
swiftui-webkit
tabletopkit
references
tipkit
references
vision-framework
weatherkit
references
widgetkit
references