# Why app numbers disagree, and how to explain it

Ad networks, the MMP, App Store Connect, Play Console, analytics and the backend each count with their own rules. Most gaps are definitional. Work through the checks in order and stop at the first one that explains the gap; then report each side's definition next to its number.

## Contents

- Ordered checks
- Date basis for aggregated iOS reports
- Counting postbacks correctly
- Cohort maturity for iOS reports
- Unknown values
- Setting a reconciliation tolerance
- Writing up the reconciliation

## Ordered checks

1. Date basis and time zone.
   - Networks often report on click or impression date, MMPs on install date, analytics on event date, aggregated iOS reports on postback-received date or a modelled install date, stores on download date. Revenue may sit on install-date cohorts in one tool and event date in another.
   - Evidence: the same 7 days pulled on the same basis and time zone from both sides. For iOS aggregated reports, see the date basis section below first.
2. Downloads against first opens.
   - Stores count downloads; MMPs and analytics count an install only when the app is first opened. Many downloads are never opened.
   - Evidence: store first-time downloads against `first_open` (or the MMP's install count) for the same days and countries.
3. Attribution windows.
   - Click and view-through windows differ between a network's own report and the MMP. On iOS, AdAttributionKit defaults to 30 days for clicks and 1 day for views, and the app may have configured different windows: click 1 to 30 days (shorter only) and view 1 to 7 days (up to 7 times longer than the default), globally or per network. A network-level entry can also ignore an interaction type.
   - Configured windows apply only to users on an OS version that supports them (iOS 18.4 per vendor coverage; confirm on Apple's pages). A window change moves counts only in that OS share.
   - Evidence: each tool's window settings and the app's `AdAttributionKitConfigurations`, with the date and release each last changed, and the share of users on 18.4 or later for the same period.
4. Overlapping claims.
   - Networks that report their own attributed installs each claim every install they touched. The MMP and Apple's frameworks pick one winner. The sum of network claims always exceeds the deduplicated total.
   - Evidence: network-claimed installs against MMP-attributed installs for the same network and dates.
5. Fraud filters and rejected installs.
   - The network counts installs the MMP blocked or rejected as invalid.
   - Evidence: the MMP's blocked or rejected install report for the same network and dates, with the rule that fired.
6. Postback delay and maturity.
   - SKAdNetwork and AdAttributionKit numbers for recent days are incomplete by design. See the maturity schedule below.
   - Evidence: the share of a cohort's expected postbacks received so far.
7. Unknown and null values.
   - Low privacy tiers and later windows send coarse or no values. Missing is not zero.
   - Evidence: share of postbacks with fine, coarse and no value, per network and week.
8. Reinstalls, redownloads and re-attribution.
   - A reinstall inside the MMP's re-attribution window is not a new install there, but it is a new first open in analytics and can be a redownload in the store. AdAttributionKit re-engagement postbacks are not installs at all.
   - Evidence: the MMP's re-attribution window setting and its change date (read it in the account; defaults differ by MMP, so verify live), the reinstall count, and redownload and re-engagement postbacks counted separately.
9. Consent and ATT coverage.
   - Users without tracking permission or consent are missing from user-level reports but can be present in aggregated ones.
   - Evidence: ATT authorized share and consent granted share for the same cohort.
10. Advertising ID availability on Android.
    - Self-attributing networks match installs to their own ad engagement through identifiers the MMP passes them, typically the advertising ID. When the ID is zeros or the `AD_ID` permission is missing, their claims fall and organic rises.
    - Evidence: `ad_id_status` shares (present, zeros, permission missing) by app release, beside network-claimed installs.
11. Revenue basis.
    - Gross price, proceeds after fees, proceeds after tax, refunds included or not, currency conversion date, one sender or two, test purchases included or not.
    - Evidence: the same five test transactions traced through each system.
12. Implementation defects, only after the definitions above are ruled out.
    - SDK start order, missing postback copy key, a postback pipeline that drops pre-version-4 SKAdNetwork postbacks, referrer failures labelled organic, S2S events sent before the install was processed, debug builds in production data.
    - Evidence: debug logs and the release record for the version where the gap started.

## Date basis for aggregated iOS reports

- SKAdNetwork and AdAttributionKit postbacks carry no install, first-launch or tap timestamp. An aggregated iOS report dates them in one of two ways:
  - Postback-received date: the day the postback arrived.
  - Modelled install date: the MMP subtracts an assumed delay from the received date.
- Before comparing or setting any maturity rule, record which basis the governing report uses and the MMP's documented offset or modelling method. Read it in the MMP's SKAdNetwork reporting documentation at run time; do not assume it.
- Align spend to the same basis. Spend on spend date divided by installs on postback-received date shifts installs later by roughly the first-postback delay (up to about 4 days without early locking).
- Failure mode: CPI moves in the opposite direction to a spend change for a few days. It looks worse after every budget increase and better after every cut. Detect it by computing CPI for the same week on spend date against postback-received date and against modelled install date; if only the received-date version swings, the basis is the cause.

## Counting postbacks correctly

- Install count: verified, unique, winning, production postbacks from the first window that are a first-time download. Deduplicate on `postback-identifier` (AdAttributionKit) or `transaction-id` (SKAdNetwork).
- AdAttributionKit: `did-win` true, `postback-sequence-index` 0, `conversion-type` `download`.
- SKAdNetwork, by the postback's `version` (missing means version 1):
  - Version 4 and later: `did-win` true, `postback-sequence-index` 0, `redownload` false.
  - Version 3: `did-win` true and `redownload` false. No sequence index exists; the postback is the first and only one.
  - Version 2: no `did-win` (only winners receive postbacks) and no sequence index; `redownload` false.
  - Version 1: no `did-win`, no sequence index, no `redownload`. Count it as an install with download type unknown and report that share beside the total.
  - Versions 1 to 3 carry `campaign-id`; version 4 and later carry `source-identifier`.
- Redownloads (AdAttributionKit `redownload`, SKAdNetwork `redownload` true) go on their own line.
- Re-engagement postbacks go in a separate re-engagement metric and are never added to installs. They can carry sequence index 0 and `did-win` true.
- Second and third window postbacks are later observations of a conversion already counted.
- Non-winning postbacks (`did-win` false) show a network qualified but lost. They are not installs.
- Development postbacks, signed with development keys, never enter production counts.
- Retried deliveries of the same postback are not new installs. AdAttributionKit retries after a 500; SKAdNetwork after any response other than 200.

Fixture: eleven deliveries arrive.

- Three distinct winning first-window AdAttributionKit `download` postbacks.
- One retried copy of one of them.
- One second-window postback.
- One non-winning postback.
- One postback failing signature validation.
- One development postback.
- One winning first-window AdAttributionKit `redownload` postback.
- One winning first-window AdAttributionKit `re-engagement` postback.
- One SKAdNetwork version 3.0 Apple Ads postback: `did-win` true, `redownload` false, `campaign-id` 10, no `postback-sequence-index`.

Expected: installs 4, redownloads 1, re-engagements 1. A pipeline that requires `postback-sequence-index` returns 3 installs; that is the defect the fixture exists to catch.

- When an MMP collects postbacks, ask which of these steps it performs, how it treats redownloads, re-engagements and pre-version-4 postbacks, and what its export represents. Never add the MMP's processed count to your own direct copies.

## Cohort maturity for iOS reports

Apply this schedule only to cohorts dated by a modelled install date. On postback-received date, each day's count is complete when it arrives but describes installs from several earlier days; use it for trend checks with spend aligned the same way, not for cohort decisions.

The schedule applies to impressions signed with SKAdNetwork 4 or AdAttributionKit. Ads signed with SKAdNetwork 3 or earlier, which includes Apple Ads while it signs with versions 1 to 3, get one winning postback and no later windows.

It follows Apple's SKAdNetwork 4 statements: the first window ends two days after first launch, the second seven days after, the third after 35 days; delays are 24 to 48 hours for the first postback and 24 to 144 hours for the others. It assumes no window is locked early and that the app sends its first conversion value update at first launch. AdAttributionKit starts the conversion window at the first update, and the device has up to 60 days to send it, so a late first update shifts everything.

Latest possible arrival, from first launch:

- First postback: 2 days plus 48 hours, about 4 days.
- Second postback: 7 days plus 144 hours, about 13 days.
- Third postback: 35 days plus 144 hours, about 41 days.
- Add the network's and the MMP's own processing lag on top. If the team wants a safety day, add it as a named team buffer.
- Locking a window sends that postback sooner, but the random delay still applies.

Rules:

- Mark iOS cohorts provisional until the last postback the decision depends on can have arrived. A budget decision built on first postbacks waits about 4 days plus processing lag; one built on third-window revenue waits about 41 days plus lag.
- Compare cohorts at the same age. A 2-day-old cohort next to a 7-day-old one is not a performance difference.
- Keep early readings and their later revisions so the team learns how unstable its own early numbers are.

## Unknown values

- Report coverage next to every rate: postbacks with fine values, coarse values and none.
- Give a range when unknowns matter. With 100 winning first postbacks, 60 fine, 20 coarse and 20 without a value, and 24 purchases among the 80 known: 30% among known, 24% of all, and 24% to 44% if every unknown could go either way.
- If coverage moves, investigate that before reading the rate change as performance.

## Setting a reconciliation tolerance

- Set each tolerance from at least 8 weeks of the observed weekly gap between the two views. Default method: the median gap plus twice its standard deviation. Name the method and the weeks used.
- Record the tolerance in `decisions.md` and review it after any SDK upgrade, attribution window change, conversion value schema change or MMP setting change.
- Where the gap should be near zero, use a fixed rule instead of a percentage: MMP gross revenue and ledger gross revenue for the same transaction references, in the original currency and with test purchases excluded, match exactly. List every unmatched reference.
- A new app with less than 8 weeks of data uses a provisional tolerance marked as such, and replaces it once 8 weeks exist.

## Writing up the reconciliation

For each disputed number, write:

- the two numbers, each with its source, date basis, time zone, window, deduplication rule and revenue basis
- the check that explains the gap, with its evidence
- the residual gap after adjustment, and whether it is within the tolerance agreed in the decision map
- which report governs the decision, unchanged unless the owner agrees to change it
