# Revenue events and store fees

How to send app revenue so attributed revenue, analytics revenue and the finance ledger can be reconciled. Facts checked against live docs on 2026-10-17. Fee schedules change often; fetch them live before quoting a rate.

## Contents

- Purchase states
- One sender per occurrence
- Store server notifications
- Firebase and GA4 specifics
- MMP revenue fields
- Store fees and proceeds
- Refunds, trials and currencies
- Reconciliation fixture

## Purchase states

A paywall view, a purchase start, a pending payment, a verified purchase, a renewal, a refund and a cancellation are different states. Only verified states create revenue. A return to the app after the store sheet does not prove a completed transaction.

## One sender per occurrence

- Each verified business occurrence (purchase, renewal, refund) has exactly one sender to each destination. The app may send intent events (`paywall_viewed`, `purchase_started`); the verified purchase comes from the backend or subscription platform, or from the app after on-device verification, but never from both.
- Keep a delivery ledger keyed to the store transaction reference. A retry reuses the occurrence; a renewal is a new occurrence of the same subscription.
- Do not rely on any SDK-level duplicate filter to protect backend retries. Check what the vendor deduplicates for each path before running two.
- AppsFlyer: customers on ROI360 store revenue should not send `af_revenue` in in-app events, because AppsFlyer warns this can report duplicate revenue; the purchase connector carries it.
- Subscription platforms often forward purchases to the MMP server-side. If one does, turn off client-side purchase revenue in the MMP SDK.
- A backend-only revenue path still has to tell the app about verified purchases and trials while the iOS conversion window is open, because conversion values are set on the device. Use a revenue-free SDK event for that signal so revenue keeps one sender. See "Conversion values are set on the device" in [ios-attribution.md](ios-attribution.md).

## Store server notifications

- Apple: App Store Server Notifications V2 report purchases, renewals, offer redemptions and refunds. Version 1 notifications are deprecated.
- Google Play: Real-time developer notifications arrive through Cloud Pub/Sub. They say only that a purchase changed state; call the Google Play Developer API (for subscriptions, `purchases.subscriptionsv2.get`) for the current state. `messageId` is unique, so deduplicate on it.
- Voided purchases on Play arrive as `voidedPurchaseNotification` with `refundType` full or quantity-based partial. The Voided Purchases API is the pull alternative.
- Rule: the backend that receives these is the source for revenue events to the MMP and analytics, and for the ledger.
- Test purchases: filter on the Apple notification's `environment` (`Sandbox` or `Production`) and the Play subscription's `testPurchase` object before sending revenue anywhere. Confirm the field names live.

## Firebase and GA4 specifics

- `in_app_purchase` is collected automatically in apps with the Firebase SDK, including an initial subscription; a free-trial start is logged with `subscription` true. Google warns its revenue can differ from Play Console figures.
- iOS: StoreKit 1 purchases are logged automatically (Analytics SDK v6.20.0 or later). StoreKit 2 needs `Analytics.logTransaction(transaction)` after a verified purchase (v12.5.0 or later). The SDK does not deduplicate a manual log against an automatic one.
- GA4 deduplicates `purchase` on `transaction_id` only for web streams. On app streams a duplicated purchase counts twice.
- Decide per app whether GA4 revenue comes from automatic `in_app_purchase`, a manual `purchase` event or a server-side feed. Write the decision down and disable the others.

## MMP revenue fields

- AppsFlyer in-app events: `af_revenue` takes a plain number with no currency symbols, commas or text, and may be negative; `af_currency` takes an ISO 4217 code and defaults to USD. AppsFlyer suggests recording cancellations as negative revenue under their own event name.
- Adjust, Branch and others have their own revenue and currency fields. Look them up live.
- Write in the tracking plan whether each revenue field is gross price, proceeds after fees, or proceeds after fees and tax.

## Store fees and proceeds

Do not hardcode a fee rate. As checked on 2026-10-17:

- Apple's App Store Small Business Program charges 15% on paid apps and in-app purchases for developers who enrol and qualify (up to 1 million USD in proceeds in the prior calendar year and in the current year). Being under the threshold is not enough; the developer has to enrol. Apple's page also offers a further reduced 10% to developers on the EU alternative terms in the programme and for subscriptions after their first year (read the page for which combination applies to the app). The standard rate returns for future sales once a developer passes the threshold in the current year.
- Google Play in Australia, the EEA, Japan, the UK and the US, rolled out on 30 June 2026 (EEA, UK, US) and 30 September 2026 (Australia, Japan). "New install" means the user's first install or first update from Google Play happened on or after the regional rollout date; "existing install" means before it.
  - Auto-renewing subscriptions: 10% plus a 5% billing fee, whatever the developer's earnings, for new and existing installs.
  - First 1 million USD of annual earnings: 10% plus a 5% billing fee on all transaction types.
  - Above that ("Standard" on the page is a row label, not a rate): other transactions pay 20% plus 5% on new installs, and 25% plus 5% on existing installs (20% for external web links).
  - Play Games Level Up or Apps Experience programmes: other transactions pay 15% plus 5% on new installs and 20% plus 5% on existing installs (15% for external web links).
  - The 5% billing fee applies to purchases made through Google Play Billing and to the initial purchase of a paid app. Alternative billing and external offers use different rates.
- Google Play in remaining markets, until the new fees roll out there: 15% on auto-renewing subscriptions whatever the earnings; developers in the 15% tier pay 15% on the first 1 million USD of annual revenue and 30% above it; some programmes offer 15% or lower on other transactions.

Rules:

- Send the gross price and currency on revenue events, as the store charged it.
- Weekly ROAS cannot wait for monthly financial reports. Keep a dated fee-rate table keyed by store, country, product type, subscription age, install age (Play new or existing install), annual earnings band, billing path and programme; apply it in the warehouse as provisional net, true it up each month from the stores' financial reports, and report the variance. Never hardcode one rate in app or MMP code.
- Prices shown to users may include VAT or sales tax depending on the region. Take the tax treatment from the store reports; do not infer it.
- Label ROAS as gross, provisional net or final net in the report name. "ROAS" on gross store price overstates margin by the fee plus tax.
- A purchase completed outside the stores has a different fee and tax basis. Record `purchase_channel` (`app_store`, `google_play`, `web`, `web_link_out` for a purchase that started from an in-app link to web checkout where the storefront allows it) on every revenue event. For web and link-out purchases the backend is the only sender.

## Refunds, trials and currencies

- Link every refund to the original transaction reference and the original attribution, not to the refund date's campaign.
- A trial start is not revenue. Send it as its own event with value 0 or no value; predicted value belongs in a separate, labelled field or model.
- Keep the original transaction currency. Convert in the warehouse with a dated rate, and never sum unconverted amounts.

## Reconciliation fixture

Illustrative QA ledger in USD with no tax or fee adjustments. Expected business treatment, not vendor output:

- Verified `qa_tx_101`, 12.00: one purchase.
- Verified `qa_tx_102`, 20.00: one purchase.
- Retry of `qa_tx_101`, 12.00: no additional purchase.
- Pending checkout, 7.00: excluded from verified purchases.
- Partial refund of `qa_tx_102`, 5.00: refund against the original transaction.

Expected: gross verified purchases 32.00 across two transactions, refunds 5.00, remainder 27.00. A counted retry gives 44.00 gross; also counting the pending checkout gives 51.00. Run the fixture through the revenue path the app actually uses and check each destination against these numbers.
