What app store revenue reports leave out
The App Store and Google Play tell you what sold. They don’t tell you what the buyer did first, or why everyone else stopped short. Here is how store revenue data works, where it ends, and what Nalya adds.
THE SHORT VERSIONEvery sale on the App Store and Google Play produces a record: a product, a price, a country, a time. That record answers “how much?” It can’t answer “why?” The answer to that sits in what people did inside your app before they paid, and in what the people who didn’t pay did instead.
What the store consoles report
App Store Connect and Google Play Console both report sales by product, country and day. You see units, proceeds after the store’s commission, new subscriptions, renewals and refunds. The numbers are accurate, and they are late. Apple’s daily Sales and Trends reports arrive the following day1. Google Play’s earnings report is monthly, typically ready by the fifth of the next month2.
Proceeds depend on the commission tier. Apple’s Small Business Program charges 15% if you earned up to 1 million USD in proceeds the year before3. Google Play lists 15% on the first $1M you earn each year, and on auto-renewing subscriptions, in most markets4. Apple’s program takes a few steps to enroll, so check that you have.
What they leave out
The store sees the moment of payment and nothing before it. It doesn’t know that the buyer used your app for three days, ran into a limit, closed the paywall once and came back. Those steps are what you can act on, and they live in your app’s analytics, not in the store.
- Day 1Installs after a searchAnalytics
- Day 1Finishes onboardingAnalytics
- Days 1 to 3Exports three projectsAnalytics
- Day 3Hits the export limit and sees the paywall, closes itAnalytics
- Day 4Exports again, opens the paywall from the export screenAnalytics
- Day 4Buys the yearly planStore console
The other half is invisible too. The store keeps no record of people who opened the paywall and left, because to the store, nothing happened. If your conversion is low, the store console can’t say whether it’s the price, the timing or a screen that never shows the offer at all.
A subscription’s life, event by event
A one-time purchase happens once. A subscription keeps changing state: it starts (often with a free trial), renews each period, and eventually ends. In between, payments fail more often than you might think. Expired cards and low balances cause what Apple calls involuntary churn: people who meant to stay but whose renewal didn’t go through.
Both stores try hard to save those subscribers, in slightly different ways:
- App Store. A failed renewal enters billing retry, and Apple keeps trying for up to 60 days. If you turn on Billing Grace Period in App Store Connect, the subscriber keeps access while Apple retries: 3 or 6 days for weekly plans, and 3, 16 or 28 days for monthly and longer ones5.
- Google Play. Grace period and account hold are both on by default. The subscriber keeps access during the grace period and loses it during account hold, which by default lasts 60 days minus the grace period6.
A cancel is not an ending either. Turning off auto-renew only means the subscription won’t renew; the subscriber keeps access until the period they paid for runs out, and can turn it back on before then.
Server notifications: the live feed
Both stores can tell your server about each of these changes as it happens, instead of in tomorrow’s report. On the App Store it’s App Store Server Notifications: you give App Store Connect an HTTPS URL, and Apple sends a signed message for each event7. On Google Play it’s Real-time developer notifications, delivered through Google Cloud Pub/Sub. Google’s messages are deliberately thin: they say a purchase changed and carry a token, and you ask the Google Play Developer API for the details8.
SUBSCRIBEDSUBSCRIPTION_PURCHASEDDID_RENEWSUBSCRIPTION_RENEWEDDID_FAIL_TO_RENEW GRACE_PERIODSUBSCRIPTION_IN_GRACE_PERIODGRACE_PERIOD_EXPIREDSUBSCRIPTION_ON_HOLDDID_RENEW BILLING_RECOVERYSUBSCRIPTION_RECOVEREDDID_CHANGE_RENEWAL_STATUS AUTO_RENEW_DISABLEDSUBSCRIPTION_CANCELEDEXPIREDSUBSCRIPTION_EXPIREDREFUNDvoidedPurchaseNotificationONE_TIME_CHARGEONE_TIME_PRODUCT_PURCHASEDRead in order, the notifications tell the story the reports flatten. Say one of your monthly subscribers has a card that just expired:
- Day 0
The renewal charge is declined. With grace period on, they keep access while the store retries.
App Store
DID_FAIL_TO_RENEWGoogle PlaySUBSCRIPTION_IN_GRACE_PERIOD - Day 2
They notice the payment problem and update their card.
- Day 2
The store charges the new card. The subscription is back.
App Store
DID_RENEWGoogle PlaySUBSCRIPTION_RECOVERED
What Nalya does with them
Nalya listens to both feeds for you. Point App Store Server Notifications (Version 2) and Google Play’s Pub/Sub push at Nalya, and every purchase, renewal, cancel, refund and expiry lands in one Revenue view, in one vocabulary across both stores. Apple’s signed messages are verified before anything is recorded, Nalya confirms the app is yours on each store, and redelivered messages are counted once.
- The last 30 days, at a glance
Purchases, renewals, and cancels with refunds and expiries, counted from the store’s own events, plus purchases for each of the last 7 days.
- Real amounts where the store gives them
App Store transactions carry the price the buyer paid9, so Nalya shows it. For Google Play, Nalya shows the buyer’s local list price from your catalog. Your store’s financial reports stay the place for exact totals.
- Told as it happens
A notification on your phone for each new purchase, subscription, renewal, cancel and refund. Sandbox purchases are recorded but never counted as revenue.
- Every purchase, findable
Search the whole history by order ID, purchase token, product or country, and filter by product, event, country or type.
Refunds follow each store’s rules. On the App Store, Apple handles refunds directly with the customer, and developers can’t issue them, so Nalya tells you where to send the buyer instead. On Google Play, you can refund a purchase from Nalya, with or without taking back access, after you confirm.
What led to this sale
Open any purchase in Nalya and you see who bought it: their device, where they were, the app version and their language. Ask “What led to this?” and Nalya reads that buyer’s events from the 45 minutes before the charge to a few minutes after, then retells them in a few plain sentences with the key moments in order, ending where the store charged the purchase. It tells only what the events show, and never adds steps that aren’t there.
This needs your app’s analytics in PostHog, connected to Nalya with a PostHog personal API key. To match a purchase to its buyer, Nalya looks for a buyer reference on the purchase itself:
- Exact match
The purchase carries your user’s ID: the
appAccountTokenon the App Store, the obfuscated account ID on Google Play10. The Nalya SDK gives you that ID, derived from the one your analytics already uses, to pass into the purchase. It comes back on the store’s own record, so the match is exact. - Likely match
No buyer reference? Nalya looks at the activity in your analytics around the time of the charge, and labels the result as likely, with how many minutes apart the two were.
- No match
If more than one person could have made the purchase, Nalya shows no buyer rather than the wrong one.
And why the others don’t
Buyers’ paths are also the best clue about everyone else. If buyers reach the paywall after their third export, and most new users stop after one, the paywall may be arriving too late for most people to ever see it. That’s a hypothesis, not an answer, and the honest way to check it is a test.
Nalya’s Experiments module runs that test on your PostHog flags: show the paywall earlier to half of new users, measure purchases, and Nalya tells you when there’s a real winner, or when it’s been flat long enough to stop.
What you need
- Your store connection. An App Store Connect API key, or a Google Play service account, so Nalya can read your catalog and check each purchase.
- Server notifications pointed at Nalya.The Version 2 URL in App Store Connect, or a Pub/Sub push subscription for Google Play. Until the first event arrives, Nalya’s Revenue page shows where to point them.
- PostHog, for the buyer’s path. Without it you still get the live feed; with it each purchase can show what led to it.
- The Nalya SDK, for exact matches.It passes your user ID with each purchase and reports one-time purchases the moment the store confirms them, so they show up even when Google Play’s one-time product notifications are off.
- Apple, Sales and Trends reports availability: daily reports are available the following day, generally by 8 a.m. Pacific Time. ↩
- Google, Download and export monthly reports: the earnings report is generated once per month and is typically available by the fifth of the following month. ↩
- Apple, App Store Small Business Program: a 15% commission for developers with up to 1 million USD in proceeds in the prior calendar year, and for developers new to the App Store. ↩
- Google, Service fees: outside the markets with their own terms, 15% on the first $1M (USD) a developer earns each year and on auto-renewing subscriptions. ↩
- Apple, Reducing involuntary subscriber churn: billing retry for up to 60 days; Billing Grace Period of 3 or 6 days for weekly subscriptions and 3, 16 or 28 days for monthly and longer ones. ↩
- Google, Subscription lifecycle: grace period and account hold are on by default; account hold defaults to 60 days minus the grace period, and access is blocked during it. ↩
- Apple, notificationType (App Store Server Notifications V2), and Enabling App Store Server Notifications. ↩
- Google, Real-time developer notifications reference: notifications arrive through Cloud Pub/Sub and only say that a purchase’s state changed; one-time product notifications are sent only if you opt in. ↩
- Apple, price: the transaction’s price in milliunits of its currency, including any subscription offer discount. ↩
- Apple, appAccountToken, and Google, setObfuscatedAccountId (up to 64 characters, no personal data in clear text). ↩