SKAN & AdAttributionKit
Two Apple-mediated attribution frameworks on iOS. The SDK writes SKAdNetwork (SKAN 2/3/4) conversion values at meaningful in-app moments — SKAN keeps working on every supported iOS version, including 17.4+. AdAttributionKit (AAK) is handled entirely server-side: Reflect receives, verifies, and decodes AAK postbacks whenever Apple sends them.
What is SKAN?
SKAdNetwork (SKAN) is Apple's privacy-preserving install attribution framework. Ad networks receive postbacks from iOS with a conversion value (0-63) that encodes post-install behavior (revenue tier, level reached, etc.). Reflect handles postback receipt and decoding server-side — the SDK's job is to update the conversion value at the right moments in your app.
AdAttributionKit (iOS 17.4+)
AdAttributionKit (AAK) is Apple's successor to SKAN, available on iOS 17.4+. AAK extends SKAN with four conversion windows, signed JWT postbacks, re-engagement support, and a new source-domain field for web-to-app attribution. On the client, the Reflect SDK deliberately does not link AdAttributionKit — ReflectSDK.UpdateConversionValue(…) writes SKAdNetwork conversion values, and SKAN postbacks continue to arrive on iOS 17.4+ devices. Server-side, Reflect fully accepts AAK postbacks at /aak-postback for the networks and campaigns that produce them; client-side AAK conversion updates are on the roadmap, not shipped.
How AAK differs from SKAN
| Aspect | SKAN 4.0 | AdAttributionKit |
|---|---|---|
| iOS version | 15.0+ (4.0 features on 16.1+) | 17.4+ |
| Postback signature | Apple-signed JSON (attribution-signature field) | JWS (ES256-signed JWT compact form) |
| Conversion windows | 3 (0-2h, 2-72h, 72-360h) | 4 (configurable, plus an explicit lock-window flag) |
| Re-engagement | Not supported | Supported (re-engagement: true in payload) |
| Web-to-app | Limited | First-class via source-domain |
| Postback URL field | NSAdvertisingAttributionReportEndpoint in Info.plist | Same Info.plist key, separate registration on Apple side |
Postback endpoint
Reflect receives AAK postbacks at:
POST https://api.reflect.cloud/aak-postback Content-Type: application/jwt Body: <compact JWS — header.payload.signature>Reflect verifies the JWS against Apple's published ES256 keys (JWKS fetched + cached in edge memory for 1h), persists the full raw JWT plus the decoded payload, and resolves the install destination via publisher-app-id → apps.store_id. Postbacks for unresolved apps are still saved to the immutable audit lake for later backfill.
How to register Reflect for your app's AAK postbacks
- In your
Info.plist, setNSAdvertisingAttributionReportEndpointtohttps://api.reflect.cloud(the path/aak-postbackis appended by Apple). - Keep the existing SKAN postback URL registered alongside — SKAdNetwork postbacks keep arriving on every supported iOS version, including 17.4+, and remain the primary Apple-mediated signal for Reflect-integrated apps. Reflect handles both at
/skan-postbackand/aak-postback. - In your app's SKAN → CV Schema page in the admin panel, define how to decode the 0-63 fine-conversion-value back into revenue tiers / engagement levels. The schema applies to both SKAN and AAK postbacks (the fine value semantics are identical).
- Within ~24 hours of your first AAK install, postbacks land in the SKAN/AAK dashboard under your app. The reporting UI distinguishes SKAN rows from AAK rows so you can compare windows.
API
// Simple — just a fine value (0-63)
ReflectSDK.UpdateConversionValue(12);
// Full — fine value + coarse + lock window + callback
ReflectSDK.UpdateConversionValue(
fineValue: 42,
coarseValue: "high", // "low", "medium", "high", or null
lockWindow: false, // true = lock the current postback window
onComplete: (ok, err) => {
if (!ok) Debug.LogWarning("SKAN update failed: " + err);
}
);Platform behavior
| Platform | Behavior |
|---|---|
| iOS 17.4+ | Still SKAdNetwork 4.0 — the SDK deliberately does not link AdAttributionKit (AAK postbacks are received server-side; client-side AAK is roadmap) |
| iOS 16.1+ | Uses SKAdNetwork 4.0 — supports fine value, coarse value, and lock window |
| iOS 15.x | Legacy SKAN — fine value only, coarse value ignored |
| Android / Editor | No-op (Editor logs the call for debugging) |
When to call it
Call UpdateConversionValue whenever the user crosses a meaningful threshold:
// Example: encode revenue tiers
void OnPurchase(double revenueUsd) {
int cv = revenueUsd >= 50 ? 63
: revenueUsd >= 20 ? 48
: revenueUsd >= 10 ? 32
: revenueUsd >= 5 ? 16
: revenueUsd >= 1 ? 8
: 4;
ReflectSDK.UpdateConversionValue(cv, coarseValue: cv >= 32 ? "high" : "medium");
}
// Example: encode level progress
void OnLevelComplete(int level) {
int cv = Math.Min(level, 63);
ReflectSDK.UpdateConversionValue(cv);
}Conversion value schema (admin)
In the admin panel, go to SKAN → CV Schema to define what each 0–63 value means (revenue ranges, engagement tiers). The schema does two jobs, and the second one is easy to miss:
- It decodes SKAN reports. Postbacks arrive carrying a bare number; the schema turns that number back into a revenue band or tier in the console and in CSV exports.
- It drives the SDK’s automatic conversion values. The iOS core underneath every Reflect SDK fetches this schema from
GET /skan/cv-schemaand caches it for 24 hours. Post-install revenue is bucketed against it, and Apple is called only when the bucket goes up. With no schema published, no automatic value is ever sent — the SDK will not invent buckets you never defined.
Your own UpdateConversionValue calls, like the ones above, are passed straight through and need no schema. But if you rely on automatic revenue-based conversion values, publishing the schema is a prerequisite rather than a reporting nicety — and editing it changes what devices send once their cached copy refreshes, within 24 hours.