Revenue and subscriptions
Designing a Paywall → Purchase → Success Subscription Funnel
Updated 2026-09-144 min read
Short answer
Log the paywall being shown (once per display, with where it was shown), the purchase being started (when the store purchase sheet is requested), the purchase being confirmed (once per transaction, after verification), and the ways it ends without success — cancelled, failed or pending. Read them as a closed funnel of users, broken down by placement, and check confirmed purchases against the store.
Many subscription apps log a paywall view and a purchase, divide one by the other, and get a number nobody can act on. It does not say whether people saw the price and left, opened the store sheet and cancelled, or paid and hit an error. Each of those needs a different fix, and each needs its own event.
The events
| Event | Log it when | Parameters worth adding |
|---|---|---|
| paywall_view | The paywall is actually displayed — once per display | placement (onboarding, settings, feature gate), paywall_id or variant |
| purchase_start | The user taps a product and the store purchase sheet is requested | product_id, placement |
| purchase_success | The transaction is verified and access is granted — once per transaction | product_id, placement, is_trial |
| purchase_cancel | The store reports that the user cancelled | product_id, placement |
| purchase_fail | The store or your verification returns an error | product_id, error_code |
| purchase_pending | The purchase waits for approval, such as Ask to Buy or a pending payment on Google Play | product_id |
Note: Firebase also collects in_app_purchase for App Store and Google Play purchases, and with StoreKit 2 you log it through Analytics.logTransaction(). Name your own event something other than purchase — purchase_success, for example — so GA4 does not count the same sale twice in its purchase revenue.
Where each event goes wrong
- paywall_view logged on every render, or every time the screen reappears, inflates views and shrinks conversion
- paywall_view logged for people who already subscribe measures a screen they cannot buy from
- purchase_start logged when the paywall opens, rather than on a tap, makes the first step a copy of the view
- purchase_success logged whenever the app finds an active subscription — on every launch, or after a restore — counts existing subscribers as new sales
- Cancellation treated as an error hides normal hesitation among real failures
Reading the funnel
Build a closed funnel exploration in GA4 with paywall_view → purchase_start → purchase_success, counting users, and break it down by placement. Each step answers a different question.
| Step | If it is low, look at |
|---|---|
| paywall_view → purchase_start | Price, trial offer, plan layout, and whether the paywall appears before people have seen any value |
| purchase_start → purchase_success | Cancellations in the store sheet, payment failures, pending approvals and verification errors |
| purchase_success by placement | Which moment in the app makes people ready to pay |
Onboarding Feature gate paywall_view (users) 5,000 800 purchase_start 400 120 purchase_success 300 60 View → success 6.0% 7.5% Start → success 75% 50%
The feature gate converts better overall, yet half of the people who try to buy there do not finish. That is a checkout problem on that path — look at purchase_cancel and purchase_fail for that placement — and no amount of price testing on the paywall would have revealed it.
Checking it against the store
Confirmed purchases in your funnel should follow the store's own records. Compare purchase_success users on the iOS stream with new subscriptions in App Store Connect over the same days, and Android with Play Console. A steady gap means an event is missing or duplicated; a sudden gap usually means a release changed where it is logged.
Careful: Free trials turn into paid subscriptions days later, usually while the app is closed. The funnel measures trial starts; paid conversion comes from App Store Connect and Play Console, or from app_store_subscription_convert where Firebase collects it.
Before you ship it
- Register placement, product_id and paywall_id as custom dimensions — event parameters do not appear in reports or explorations until you do, and registration does not fill in earlier data
- Mark purchase_success as a key event
- On one device in DebugView, walk through view, start, cancel, failure and success, and confirm each event arrives exactly once
- Use sandbox accounts on iOS and license testers on Google Play for the walkthrough; GA4 ignores sandbox purchases for in_app_purchase, so the tests do not pollute revenue
How this looks in Appnaly
In Appnaly you can mark a start event and a completion event as the core journey of an app — purchase_start and purchase_success fit well. Appnaly then asks GA4's funnel report for the people who did both, in order, compares the rate with the previous completed week, and stays quiet until there are enough people for a change to mean something.