CHURN METRICSGOOGLE PLAY

Tracking churn with Google Play real-time developer notifications

How to measure Android subscription churn with Google Play real-time developer notifications: Pub/Sub setup, what each event means, and churn metrics.

Published 5 min read

Google Play tells your backend about subscription changes through real-time developer notifications (RTDN), published to a Google Cloud Pub/Sub topic. They are the basis of any serious churn tracking for an Android app: who canceled, who is in a billing problem, who paused, who actually left.

There is a catch that shapes everything else: a notification only says that something changed. This guide covers the setup, how to process each message, how notification types map to churn metrics, and the mistakes that skew the numbers. It is based on Google’s documentation: configuring RTDN, the RTDN reference, the subscription lifecycle and the API reference for purchases.subscriptionsv2 (all accessed November 2026).

Setting up notifications

Google’s setup guide walks through it:

  1. Create a Pub/Sub topic in a Google Cloud project.
  2. Give the service account google-play-developer-notifications@system.gserviceaccount.com the Pub/Sub Publisher role on the topic.
  3. Create a subscription on the topic: push to an HTTPS endpoint on your backend, or pull from a worker.
  4. In the Play Console, enter the topic name for your app and click Send Test Message.
  5. Choose the notification types: subscriptions and voided purchases only, or everything including one-time products.

Acknowledge every message. For pull subscriptions, Google says to acknowledge pulled messages “to avoid repeated delivery by Cloud Pub/Sub”; for push, a successful response code serves as the acknowledgement.

Processing a message

Each Pub/Sub message has a base64 data field. Decoded, it contains version, packageName, eventTimeMillis and exactly one of subscriptionNotification, oneTimeProductNotification, voidedPurchaseNotification, pendingRefundReviewNotification or testNotification.

A subscription notification carries only a notificationType and a purchaseToken. Google’s reference is explicit: you “must call the Google Play Developer API after receiving Real-time developer notifications to get the complete status,” because notifications “do not give you complete information about the purchase.” The lifecycle guide calls purchases.subscriptionsv2.get “the source of truth for subscription management.”

So the pipeline is:

  1. Receive, check messageId for duplicates (Google recommends it), store the raw message, acknowledge.
  2. Call purchases.subscriptionsv2.get with the purchase token.
  3. Store the resulting state as a snapshot with the notification type and eventTimeMillis.
  4. Update your entitlement and your metrics from the snapshot, not from the notification alone.

Keep in mind that the purchase token works only until 60 days after expiration. Fetch the state when the notification arrives, not weeks later.

The subscription notification types

From the RTDN reference:

Code Type What happened
1 SUBSCRIPTION_RECOVERED Recovered from account hold, or resumed from pause
2 SUBSCRIPTION_RENEWED An active subscription renewed
3 SUBSCRIPTION_CANCELED Canceled, voluntarily or involuntarily
4 SUBSCRIPTION_PURCHASED New subscription purchased
5 SUBSCRIPTION_ON_HOLD Entered account hold
6 SUBSCRIPTION_IN_GRACE_PERIOD Entered grace period
7 SUBSCRIPTION_RESTARTED User restored a canceled, not yet expired subscription
9 SUBSCRIPTION_DEFERRED Renewal date extended
10 SUBSCRIPTION_PAUSED Pause took effect
11 SUBSCRIPTION_PAUSE_SCHEDULE_CHANGED Pause scheduled or changed
12 SUBSCRIPTION_REVOKED Revoked before expiry
13 SUBSCRIPTION_EXPIRED Expired
18 SUBSCRIPTION_CANCELLATION_SCHEDULED Installment plan cancellation scheduled for the end of the commitment
20 SUBSCRIPTION_PENDING_PURCHASE_CANCELED A pending transaction was canceled

There are a few more (items changed, price change updates, price step-up consent). Code 8 is deprecated.

The states that matter for churn

The subscriptionState field from the API is more reliable than the notification type for counting. The lifecycle guide describes what each state means for access:

State User has access? Churn meaning
SUBSCRIPTION_STATE_ACTIVE Yes Subscriber
SUBSCRIPTION_STATE_IN_GRACE_PERIOD Yes At risk: payment failed
SUBSCRIPTION_STATE_ON_HOLD No At risk: payment failed, access blocked
SUBSCRIPTION_STATE_PAUSED No Paused, not churned
SUBSCRIPTION_STATE_CANCELED Until expiryTime At risk: renewal stopped
SUBSCRIPTION_STATE_EXPIRED No Churned

Grace period and account hold come with defaults worth knowing. Grace period is enabled by default on all auto-renewing base plans. Account hold is also on by default, with a length Google calculates as 60 days minus the grace period. Both can be changed in the Play Console. Before account hold starts, Google makes additional charge attempts for up to 48 hours.

From events to churn metrics

Count churn at expiry

SUBSCRIPTION_CANCELED means renewal stopped, not that the user left. They keep access until expiryTime, and can restore it (SUBSCRIPTION_RESTARTED). Count churn on SUBSCRIPTION_EXPIRED, and use cancellations as a leading indicator.

Split voluntary and involuntary churn

The subscription resource’s canceledStateContext says who canceled:

  • userInitiatedCancellation: voluntary. It may also contain the user’s cancel survey answer.
  • systemInitiatedCancellation: canceled by the system, “for example because of a billing problem”. This is your involuntary churn.
  • developerInitiatedCancellation: you canceled.
  • replacementCancellation: replaced by a new subscription, for example after a plan change. Not churn.

The billing path has a recognizable sequence: SUBSCRIPTION_IN_GRACE_PERIOD, then SUBSCRIPTION_ON_HOLD, then either SUBSCRIPTION_RECOVERED or, at the end of account hold, SUBSCRIPTION_CANCELED followed immediately by SUBSCRIPTION_EXPIRED.

Don’t count pauses as churn

SUBSCRIPTION_RECOVERED fires both when a subscription leaves account hold and when it resumes from a pause. Check the previous state before counting it as a billing recovery.

Handle plan changes and re-signups

Upgrades, downgrades and in-app re-signups before expiry create a new purchase with a new token. The new subscription has a linkedPurchaseToken pointing to the old one. If you don’t follow these links, every plan change looks like one churned subscriber and one new subscriber.

Resubscriptions from the Play Store after expiry are different: Google says they do not carry a linkedPurchaseToken, but the API response includes outOfAppPurchaseContext with the expired token until you acknowledge the purchase. Count those as reactivations.

Revocations

SUBSCRIPTION_REVOKED means access ended early: you revoked it, or the purchase was charged back. The state becomes expired. Report it separately from normal churn, alongside voided purchases.

A monthly report

  1. Active at start: subscriptions in active, grace period, or canceled-but-not-expired states.
  2. Voluntary churn: expirations whose context is user-initiated.
  3. Involuntary churn: expirations whose context is system-initiated.
  4. Billing recovery rate: subscriptions that entered grace period or account hold and were recovered, over all that entered.
  5. Restores: SUBSCRIPTION_RESTARTED.
  6. Paused: count, and how many resume.
  7. Reactivations: Play Store resubscriptions and in-app re-signups after expiry.

The general formulas are in how to calculate churn rate.

Common mistakes

  • Trusting the notification type alone. Always fetch the subscription state.
  • Counting cancellations as churn. It inflates churn and hides restores.
  • Ignoring linked purchase tokens. Plan changes become fake churn.
  • Calling the API too late. Tokens stop working 60 days after expiration.
  • Not acknowledging Pub/Sub messages. You get duplicates and wasted API quota.
  • No obfuscated account ID at purchase. You cannot join Play data with your own user data.

Doing this with NoChurn

You point your Play real-time developer notifications to NoChurn and give it a service account with access to your Play Console. NoChurn fetches the subscription state for each event, follows linked purchase tokens, separates voluntary, billing and revoked churn, and keeps a timeline per customer, matched through the obfuscated account ID you set at purchase. Grace period, account hold and cancellations feed the churn score from 0 to 100, and you can add your own attributes by API. Google keeps payments, retries and the cancel button. See NoChurn + Google Play.

PROMPT

Read https://www.nochurn.sh/llms.txt,
then follow https://www.nochurn.sh/docs/quickstart/
to add NoChurn to this app. Use a test key
(nc_test_…) until I say we go live.

Stop leaking revenue.

  • Free until $300 MRR
  • No card needed
  • Live in 10 minutes
  • Never a cut of what we recover