Designing a SKAdNetwork Conversion Value Schema That Works
Your SKAdNetwork conversion value schema is a table that maps user behavior in the first hours after install onto a single 6-bit number (0–63). Design it in three moves: pick one primary signal to encode (early revenue, a funnel sequence, or a predicted value), budget the 64 fine values across that signal, then define a coarse fallback (none/low/medium/high) that still means something when Apple downgrades you. Everything else is haggling over bucket edges.
I've shipped this schema into apps big enough that the postback volume was never the problem, and into ones where half the installs came back null. The framework is the same. The stakes just get uglier the smaller you are.
What the OS actually hands you
Let me walk through what happens on the device, because the schema only makes sense once you see the plumbing.
A user installs your app from an ad. Your SDK calls updatePostbackConversionValue and hands StoreKit a fine value and a coarse value. StoreKit doesn't send anything yet. It starts a timer. Every time you update the value inside the measurement window, the timer resets and the window can extend, up to a hard ceiling of two days for the first postback. When the window finally closes, Apple sits on the postback for a random delay (24 to 48 hours for postback one) and then mails it to the ad network. Postbacks two and three cover later activity and get sent with a random delay of anywhere from 24 to 144 hours, per Apphud's SKAN 4 breakdown.
So you never control when the data arrives. My running joke on every attribution team I've been on: the postback shows up whenever it feels like it, and it's usually a Tuesday. Plan your reporting around that, not around install day.
The part that trips people up: only the first postback can carry your fine value. Postbacks two and three are coarse only. Adjust's SKAN 4 documentation is clear on this. All that six-bit granularity you're about to design lives inside a two-day window, and it only survives if the campaign clears Apple's crowd-anonymity bar.
The tier problem is the real constraint
Here's the thing nobody tells you before you design your first schema: your beautiful 64-value table is conditional. SKAN 4 assigns every install a crowd-anonymity tier from 0 to 3, based on how many other installs look similar (same source app, same ad network, same source identifier granularity). The tier decides how much data you actually get.
Per Adjust's crowd-anonymity tiers guide, it breaks down roughly like this:
| Tier | First postback carries | Postbacks 2 & 3 |
|---|---|---|
| 3 (high) | Fine value (0–63) + source ID | Coarse value sent |
| 2 | Fine value on most installs | Coarse value sent |
| 1 (medium) | Coarse value only | Coarse value sent |
| 0 (low) | Coarse value only | Not sent at all |
Read that bottom row twice. At tier 0 you get one coarse bucket and nothing else, ever. AppsFlyer's SKAN data learnings put overall null-value rates around 10–15%, but that number hides the distribution. Campaigns under roughly 100 installs a day routinely fall below the threshold and eat nulls or coarse-only postbacks. If you're a small advertiser, you're designing for tier 0 and 1 whether you like it or not.
Which leads to the first design rule that most tutorials skip: your coarse buckets have to carry weight on their own, because for a big chunk of your spend, coarse is all you'll ever see.
Step 1: pick one primary signal
You can encode exactly one thing well in six bits. The Admiral Media SKAN guide lays out the three honest options, and they don't mix cleanly:
- Revenue bands. Map the 0–63 range to dollar ranges of early in-app revenue. Best for apps that monetize fast — games with IAP, one-time purchases.
- Funnel milestones. Encode the furthest step a user reached: onboarded, added payment, started trial, subscribed. Best for apps where the money comes later than 48 hours.
- Predicted value. Run a model that scores the user's likely long-term value from their early behavior, then bucket the prediction. Best if you already have a working predicted-LTV model and enough scale to trust it inside two days.
People try to jam all three into one integer with bit-packing — three bits for revenue, two for funnel, one for a trial flag. I've done it. It fights crowd anonymity, because every extra distinct value you emit thins out the population that shares that value and pushes more installs down a tier. Fewer distinct values means fatter, more anonymous buckets means more surviving fine postbacks. Restraint is a measurement feature here, not a limitation.
Pick the axis that matches when your revenue actually lands. If most of your money arrives inside two days, encode revenue. If it doesn't, encode the funnel and stop pretending SKAN can see your week-four subscriber. It can't.
Step 2: budget the 64 fine values
Here's a reusable hybrid schema I've used for a subscription-plus-IAP app. It leans on funnel milestones early, then hands the top of the range to revenue. Values are the fine conversion value written on postback one.
| Fine CV | Category | Condition inside the 0–48h window |
|---|---|---|
| 0 | None | Install only, no tracked event |
| 1 | Activation | Completed onboarding |
| 2 | Activation | Reached core action (e.g. first cart add) |
| 3 | Intent | Started free trial |
| 4 | Intent | Added a payment method |
| 5 | Revenue | $0.01–$0.99 |
| 6 | Revenue | $1.00–$4.99 |
| 7 | Revenue | $5.00–$9.99 |
| 8 | Revenue | $10.00–$19.99 |
| 9 | Revenue | $20.00–$49.99 |
| 10 | Revenue | $50.00–$99.99 |
| 11 | Revenue | $100.00–$199.99 |
| 12 | Revenue | $200.00+ |
| 13–63 | Reserved | Split revenue bands finer as you learn |
Notice how little of the range I've spent. That's deliberate. A schema with 13 live values clears crowd anonymity far more often than one with 60. You can always widen — carve band 6 into $1–2.49 and $2.50–4.99 once you confirm those installs survive at tier 2. Start coarse, earn granularity. Firebase's revenue-bucket codelab sets the floor at a minimum of five buckets with bucket one pinned to $0.00, and that's a sane place to begin even if you never grow past a dozen.
One rule the SDK won't enforce for you: the value must be monotonic and non-overlapping. A user at $12 gets exactly one value (8), never a choice between the funnel value and the revenue value. When someone both starts a trial and spends, revenue wins — take the higher-value event. If your update logic can emit two different values for the same user state, your model builder downstream will quietly mis-bucket, and you won't notice until the revenue reconciliation is off and everyone's blaming the ad network.
Step 3: make coarse mean something
The coarse value is not an afterthought. For low-volume campaigns and for every second and third postback, it's the whole signal. Design it to collapse your fine schema into four honest bands:
| Coarse value | Summarizes fine range | Reads as |
|---|---|---|
| none | 0 | Installed, did nothing |
| low | 1–2 | Activated, no intent |
| medium | 3–4 | Showed purchase intent |
| high | 5–12 | Paid |
Now even a tier-0 install tells you "paid vs. showed intent vs. did nothing," which is enough to keep a small campaign from flying completely blind. Per Adjust, medium crowd anonymity gives you exactly this coarse value on the first postback and nothing finer, so treat coarse as a first-class part of the schema, not a downgrade you tolerate.
If your coarse bands don't line up with a clean cut in your fine table, that's a signal your fine table is encoding two things at once. Go back to step one.
The measurement window tradeoff
The window is where I see the most self-inflicted damage. You choose how long StoreKit keeps the conversion-value timer open before it locks postback one. Longer windows catch more behavior; shorter windows get you data sooner and, more importantly, lock in the value before a user churns out of a bucket.
Roughly how the tradeoff shakes out:
| Window | You capture | You lose |
|---|---|---|
| ~24h | Fast IAP, onboarding, first session revenue | Anything that happens on day 2 |
| ~48h (postback-1 max) | Trial starts, second-session purchases | Still blind past 48h; slower to report |
| Later behavior (days 3–35) | Renewals, delayed conversions | Coarse only — no fine value, ever |
The trap: teams pick a 48-hour window because it "captures more," then wonder why their fine-value distribution is mush. A longer window means every update resets the timer, which delays the postback and can let a user's early $5 purchase get overwritten by a later state. For most apps I'd anchor the fine value on a 24-hour window for anything revenue-driven, and only stretch toward 48 if your activation genuinely happens on day two. Then lean on postbacks two and three, coarse though they are, for the renewal story. Just remember they arrive on their own schedule, which is to say, whenever they feel like it.
Where your existing analytics data helps
You don't have to guess which events belong in the schema. The whole exercise is really a question your product data can answer: which early events actually correlate with the value you care about at day 30? If your event stream already lives in a product-analytics tool (Kixo, for one, is chat-first and holds your funnel and revenue events, so you can just ask which events predict week-four retention before you hard-code them into six bits), pull that correlation first, then design the table around the events that earn their bit. Whatever you use, do the analysis before you freeze the schema, not after the first postbacks disappoint you.
For the broader picture of how this schema fits alongside deterministic and modeled attribution, see our privacy-first attribution reference architecture.
A short FAQ
How many fine values should I actually use? Fewer than you think. Start with 10–15 live values so more installs clear crowd anonymity, then split buckets once you confirm the finer ones survive at tier 2 or higher. A schema that emits 60 distinct values on a small campaign mostly emits nulls.
Can I change the schema later? Yes, but you can't retroactively re-read old postbacks. A schema change creates a discontinuity in your historical data, so version it, note the switch date, and don't compare fine-value distributions across the boundary.
What if most of my postbacks are coarse or null? That's crowd anonymity, not a bug. Consolidate campaigns to pool installs above the threshold, and make sure your four coarse bands are individually actionable. If you're consistently under ~100 installs/day per campaign, design for coarse first and treat any fine value you get as a bonus.
Does the fine value work on postbacks two and three? No. Only the first postback can carry a fine value, and only at high crowd anonymity. Two and three are coarse only. Build your late-funnel measurement expecting three brightness levels, not sixty-four.
The schema isn't the hard part. Living with two-day windows, random delays, and a tier system that quietly redacts your data — that's the part that leaves scars. Design for the downgrade, keep the value count honest, and let the postbacks show up whenever they get around to it.