Managing subscription lifecycles on Android is one of the most complex aspects of in-app billing implementation. Subscriptions go through numerous states throughout their lifetime, from initial purchase to renewals, grace periods, account holds, pauses, cancellations, and eventual expiration. Each state transition requires specific handling in your app to ensure users receive the correct entitlements, while your backend maintains accurate subscription records. Understanding these lifecycle events is essential for building a robust subscription system that minimizes involuntary churn and provides a seamless user experience.
To make it a little clearer, you will break down the complete Google Play subscription lifecycle in depth. We’ll cover every subscription state and the transitions between them, examine how Real-Time Developer Notifications (RTDN) inform your backend of changes, understand the differences between auto-renewing and prepaid subscriptions, and see how proper lifecycle handling can recover revenue from failed payments. Finally, we’ll look at how RevenueCat simplifies this complexity by abstracting away much of the lifecycle management.
The subscription lifecycle at a glance
Before diving into the details, let’s establish a mental model of how subscriptions flow through different states. A subscription begins with a purchase, enters an active state, and eventually either renews successfully, encounters payment issues, gets canceled, or expires. The complexity arises from the numerous intermediate states and recovery mechanisms that Google Play provides.

Each state has specific implications for user entitlements and requires different handling in your app and backend.
New subscription purchases
When a user purchases a subscription, your app receives a SUBSCRIPTION_PURCHASED notification, and the subscription enters the SUBSCRIPTION_STATE_ACTIVE state. This is the starting point of the lifecycle.
The critical acknowledgment requirement
One of the most important aspects of handling new purchases is acknowledgment. Google Play requires you to acknowledge a subscription purchase within three days of the transaction. If you fail to acknowledge within this window, the user automatically receives a refund, and the subscription is revoked.
1private fun processPurchase(purchase: Purchase) {
2 if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
3 // Verify the purchase with your backend first
4 verifyPurchaseWithBackend(purchase) { isValid ->
5 if (isValid && !purchase.isAcknowledged) {
6 val params = AcknowledgePurchaseParams.newBuilder()
7 .setPurchaseToken(purchase.purchaseToken)
8 .build()
9
10 billingClient.acknowledgePurchase(params) { result ->
11 if (result.responseCode == BillingClient.BillingResponseCode.OK) {
12 grantEntitlement(purchase)
13 }
14 }
15 }
16 }
17 }
18}
The acknowledgment serves as confirmation that you have granted the user access to their purchased content. It’s a safeguard that protects users from situations where a purchase succeeds on Google’s side but fails to register in your app.
Understanding the purchase response
When you query a new subscription purchase using the purchases.subscriptionsv2.get API endpoint, you receive detailed information about the subscription state:
| Field | Value for new purchase |
| subscriptionState | SUBSCRIPTION_STATE_ACTIVE |
| acknowledgementState | ACKNOWLEDGEMENT_STATE_PENDING |
| autoRenewEnabled | true |
| expiryTime | Next renewal date |
The expiryTime field indicates when the current billing period ends and renewal will be attempted. For a monthly subscription purchased on January 15, this would be February 15.
Linking purchases to user accounts
Google Play provides ExternalAccountIdentifiers to help you associate purchases with user accounts in your system. When configuring the billing flow, you can pass your internal user ID:
1val billingFlowParams = BillingFlowParams.newBuilder()
2 .setProductDetailsParamsList(productDetailsParamsList)
3 .setObfuscatedAccountId(userId.hashCode().toString())
4 .setObfuscatedProfileId(profileId.hashCode().toString())
5 .build()
These identifiers are returned in the purchase response and RTDN notifications, allowing your backend to correctly attribute purchases to user accounts even in edge cases like upgrades, downgrades, or resubscriptions.
Subscription renewals
For auto-renewing subscriptions, Google Play automatically attempts to charge the user’s payment method when the billing period ends. Successful renewals trigger a SUBSCRIPTION_RENEWED notification.
What happens on successful renewal
When a subscription renews successfully, the subscription remains in SUBSCRIPTION_STATE_ACTIVE, and the expiryTime is updated to reflect the new billing period. Importantly, renewals do not require acknowledgment, only the initial purchase does.
Your backend should update the stored expiry time when receiving the renewal notification:
1fun handleRenewalNotification(notification: SubscriptionNotification) {
2 val purchaseToken = notification.purchaseToken
3
4 // Query latest subscription state
5 val subscription = playDeveloperApi
6 .purchases()
7 .subscriptionsv2()
8 .get(packageName, purchaseToken)
9 .execute()
10
11 // Update stored expiry time
12 val newExpiryTime = subscription.lineItems[0].expiryTime
13 subscriptionRepository.updateExpiryTime(purchaseToken, newExpiryTime)
14}
Renewal date behavior
Google Play follows specific rules for renewal dates that you should be aware of:
- Subscriptions started on 29, 30, or 31 of a month will renew on day 28 (or 29 in leap years) when the following month has fewer days
- Once a renewal shifts to an earlier date (like 28), it stays on that date for subsequent months
- For example, a subscription starting March 31 renews April 30, then May 30, June 30, and so on
This behavior can affect analytics if you’re tracking renewal patterns, so keep it in mind when building reports.
Grace periods: the first line of defense
When a renewal payment fails, Google Play doesn’t immediately suspend the subscription. Instead, it enters a grace period: a recovery window during which the user retains full access while Google retries the payment.
Grace period configuration
Grace periods are enabled by default in the Play Console and can be configured per subscription:
| Billing period | Available grace period options |
| Weekly | 3, 7 days |
| Monthly | 7, 14, 30 days |
| Annual | 7, 14, 30 days |
During the grace period, the subscription state changes to SUBSCRIPTION_STATE_IN_GRACE_PERIOD, but autoRenewEnabled remains true because the user hasn’t canceled; they just have a payment issue.
Handling grace period in your app
When you detect a user is in the grace period, you should encourage them to update their payment method. Google provides the In-App Messaging API to display a standardized payment update dialog:
1fun checkAndShowGracePeriodMessage(activity: Activity) {
2 val inAppMessageParams = InAppMessageParams.newBuilder()
3 .addInAppMessageCategoryToShow(InAppMessageParams.InAppMessageCategoryId.SUBSCRIPTION_GRACE_PERIOD)
4 .build()
5
6 billingClient.showInAppMessages(activity, inAppMessageParams) { result ->
7 if (result.responseCode == InAppMessageResult.InAppMessageResponseCode.NO_ACTION_NEEDED) {
8 // No message was shown - user might have already fixed payment
9 }
10 }
11}
The key point is that users retain access during the grace period. Your app should continue providing the subscribed features while simultaneously nudging users to fix their payment method.
Silent grace period
Even if you configure a zero–day grace period in the Play Console, Google Play still provides a minimum one–day silent grace period for payment processing retries. During this silent grace period, the subscription appears as SUBSCRIPTION_STATE_ACTIVE (not IN_GRACE_PERIOD).
This is important to understand because you might receive a delayed notification about payment issues. After 24 hours, the subscription will transition to one of several states depending on the outcome:
SUBSCRIPTION_RENEWEDif the retry succeededSUBSCRIPTION_ON_HOLDif account hold is enabledSUBSCRIPTION_CANCELEDif the user canceled during this timeSUBSCRIPTION_EXPIREDif no recovery mechanisms are enabled
Account hold: the second chance
If the grace period expires without successful payment recovery, the subscription enters account hold. This state represents a more serious payment failure where the user loses access to subscribed content.
Account hold duration
Account hold is enabled by default with a duration of 60 days minus the grace period length. For example, if you have a seven–day grace period, account hold lasts 53 days. During this time:
- The subscription state is
SUBSCRIPTION_STATE_ON_HOLD - The
expiryTimeis set to a past timestamp - The subscription is not returned by
queryPurchasesAsync() - The user should lose access to premium features
User experience during account hold
Your app should detect when a user’s subscription is on hold and display appropriate messaging. Since subscriptions on hold are not returned by queryPurchasesAsync(), you need to query your backend or the Google Play Developer API directly to check for this state:
1class SubscriptionManager(
2 private val billingClient: BillingClient,
3 private val backendApi: BackendApi
4) {
5 suspend fun checkSubscriptionStatus(userId: String): SubscriptionState {
6 // First, check active purchases from Play Billing
7 val activePurchases = queryActivePurchases()
8
9 if (activePurchases.isNotEmpty()) {
10 return SubscriptionState.Active(activePurchases.first())
11 }
12
13 // No active purchases found - check backend for account hold
14 // Your backend should track subscription state from RTDN
15 val backendStatus = backendApi.getSubscriptionStatus(userId)
16
17 return when (backendStatus.state) {
18 "SUBSCRIPTION_STATE_ON_HOLD" -> {
19 SubscriptionState.OnHold(
20 purchaseToken = backendStatus.purchaseToken,
21 holdStartTime = backendStatus.holdStartTime
22 )
23 }
24 "SUBSCRIPTION_STATE_PAUSED" -> {
25 SubscriptionState.Paused(
26 resumeTime = backendStatus.autoResumeTime
27 )
28 }
29 "SUBSCRIPTION_STATE_CANCELED" -> {
30 SubscriptionState.Canceled(
31 expiryTime = backendStatus.expiryTime
32 )
33 }
34 else -> SubscriptionState.None
35 }
36 }
37
38 private suspend fun queryActivePurchases(): List<Purchase> {
39 return suspendCoroutine { continuation ->
40 billingClient.queryPurchasesAsync(
41 QueryPurchasesParams.newBuilder()
42 .setProductType(BillingClient.ProductType.SUBS)
43 .build()
44 ) { billingResult, purchases ->
45 if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
46 continuation.resume(purchases)
47 } else {
48 continuation.resume(emptyList())
49 }
50 }
51 }
52 }
53}
54
55// In your Activity or ViewModel
56fun updateUIForSubscriptionState() {
57 lifecycleScope.launch {
58 when (val state = subscriptionManager.checkSubscriptionStatus(userId)) {
59 is SubscriptionState.Active -> {
60 showPremiumContent()
61 }
62 is SubscriptionState.OnHold -> {
63 // Show payment recovery UI with deep link to Play Store
64 showAccountHoldMessage()
65 showFixPaymentButton(state.purchaseToken)
66 }
67 is SubscriptionState.Paused -> {
68 showPausedMessage(state.resumeTime)
69 }
70 is SubscriptionState.Canceled -> {
71 // Still has access until expiry
72 showCanceledMessage(state.expiryTime)
73 showPremiumContent()
74 }
75 is SubscriptionState.None -> {
76 showSubscriptionOffers()
77 }
78 }
79 }
80}
This implementation requires some infrastructure: your backend must receive and process RTDN notifications, store subscription state, and expose an API for your app to query. You also need to handle edge cases like network failures, state synchronization, and ensuring your backend state matches Google Play’s state.
With RevenueCat SDK, this complexity is taken away entirely across platforms. RevenueCat processes RTDN notifications on your behalf and maintains subscription state that you can query with a single call:
1fun checkSubscriptionStatus() {
2 Purchases.sharedInstance.getCustomerInfoWith { customerInfo ->
3 val entitlement = customerInfo.entitlements["premium"]
4
5 when {
6 entitlement?.isActive == true -> {
7 showPremiumContent()
8 }
9 entitlement?.billingIssueDetectedAt != null -> {
10 // RevenueCat detected account hold or grace period
11 showPaymentRecoveryUI(customerInfo.managementURL)
12 }
13 entitlement != null -> {
14 // Subscription exists but expired
15 showResubscribeOptions()
16 }
17 else -> {
18 showSubscriptionOffers()
19 }
20 }
21 }
22}
RevenueCat’s CustomerInfo automatically reflects the current subscription state, including account hold status via the billingIssueDetectedAt field. The managementURL property provides a direct link to Google Play’s subscription management screen where users can fix their payment method.
During account hold, users can still cancel, restore, or resubscribe. If they fix their payment method, you’ll receive a SUBSCRIPTION_RECOVERED notification, and the subscription returns to SUBSCRIPTION_STATE_ACTIVE with the same purchase token — it’s a recovery, not a new purchase.
The billing date reset
An important detail: when a subscription recovers from account hold, the billing date resets to the recovery date. If a user’s subscription was originally set to renew on day 15 of each month, but they recover from account hold on day 22, their new renewal date becomes day 22.
Subscription pausing
Google Play allows users to pause their subscriptions — a feature that can reduce cancellations by giving users a temporary break without losing their subscription entirely.
Pause configuration and availability
Pause functionality is enabled by default in the Play Console but is only available for certain billing periods:
| Billing Period | Available Pause Durations |
| Weekly | 1, 2, 3, 4 weeks |
| Monthly | 1, 2, 3 months |
| Three-month | 1, 2, 3 months |
| Six-month | 1, 2, 3 months |
| Annual | Not available |
Annual subscriptions cannot be paused, this is a deliberate limitation because the pause period could potentially exceed the subscription duration.
The pause lifecycle
Pausing involves multiple notifications as the pause progresses through its stages:
- User initiates pause:
SUBSCRIPTION_PAUSE_SCHEDULE_CHANGEDnotification- State remains
SUBSCRIPTION_STATE_ACTIVE - User retains access until current billing period ends
- State remains
- Pause takes effect:
SUBSCRIPTION_PAUSEDnotification- State becomes
SUBSCRIPTION_STATE_PAUSED - User loses access
PausedStateContextcontains the expected resume date
- State becomes
- Subscription resumes:
SUBSCRIPTION_RECOVEREDnotification- Can be automatic (pause period ends) or manual (user resumes early)
- State returns to
SUBSCRIPTION_STATE_ACTIVE
If the user manually resumes their subscription before the pause period ends, their billing date changes to the date they resumed. This is similar to the billing date reset that occurs after account hold recovery.
Handling pause in your app
Handling subscription pauses requires tracking multiple states and coordinating between your app and backend. The challenge is that paused subscriptions behave differently from other non-active states, a scheduled pause still grants access, while an active pause does not.
Your backend needs to process RTDN notifications to track pause state changes:
1// Backend notification handler
2class SubscriptionNotificationHandler(
3 private val subscriptionRepository: SubscriptionRepository,
4 private val playDeveloperApi: AndroidPublisher
5) {
6 fun handleNotification(notification: DeveloperNotification) {
7 val purchaseToken = notification.subscriptionNotification.purchaseToken
8
9 when (notification.subscriptionNotification.notificationType) {
10 NotificationType.SUBSCRIPTION_PAUSE_SCHEDULE_CHANGED -> {
11 // User scheduled a pause from Play Store subscription settings
12 val subscription = fetchSubscriptionDetails(purchaseToken)
13 val pauseInfo = subscription.lineItems[0].autoRenewingPlan?.pausedInfo
14
15 if (pauseInfo != null) {
16 // Pause is scheduled - record when it will take effect
17 subscriptionRepository.updatePauseSchedule(
18 purchaseToken = purchaseToken,
19 pauseScheduledAt = Instant.now(),
20 pauseEffectiveAt = subscription.lineItems[0].expiryTime,
21 autoResumeTime = pauseInfo.autoResumeTime
22 )
23 } else {
24 // User canceled the scheduled pause
25 subscriptionRepository.clearPauseSchedule(purchaseToken)
26 }
27 }
28
29 NotificationType.SUBSCRIPTION_PAUSED -> {
30 // Pause is now active - user loses access
31 val subscription = fetchSubscriptionDetails(purchaseToken)
32 val pausedContext = subscription.pausedStateContext
33
34 subscriptionRepository.updateSubscriptionState(
35 purchaseToken = purchaseToken,
36 state = SubscriptionState.PAUSED,
37 autoResumeTime = pausedContext?.autoResumeTime
38 )
39 }
40
41 NotificationType.SUBSCRIPTION_RECOVERED -> {
42 // Could be recovery from pause, account hold, or grace period
43 val subscription = fetchSubscriptionDetails(purchaseToken)
44
45 subscriptionRepository.updateSubscriptionState(
46 purchaseToken = purchaseToken,
47 state = SubscriptionState.ACTIVE,
48 expiryTime = subscription.lineItems[0].expiryTime
49 )
50 }
51 }
52 }
53
54 private fun fetchSubscriptionDetails(purchaseToken: String): SubscriptionPurchaseV2 {
55 return playDeveloperApi
56 .purchases()
57 .subscriptionsv2()
58 .get(packageName, purchaseToken)
59 .execute()
60 }
61}
On the client side, your app needs to query the backend to determine the current pause state and display appropriate UI:
1class PauseStateManager(
2 private val backendApi: BackendApi,
3 private val billingClient: BillingClient
4) {
5 sealed class PauseState {
6 object NotPaused : PauseState()
7 data class PauseScheduled(
8 val currentPeriodEnd: Instant,
9 val autoResumeTime: Instant
10 ) : PauseState()
11 data class ActivelyPaused(
12 val autoResumeTime: Instant
13 ) : PauseState()
14 }
15
16 suspend fun checkPauseState(userId: String): PauseState {
17 val subscriptionStatus = backendApi.getSubscriptionStatus(userId)
18
19 return when (subscriptionStatus.state) {
20 "SUBSCRIPTION_STATE_ACTIVE" -> {
21 // Check if pause is scheduled
22 if (subscriptionStatus.pauseScheduledAt != null) {
23 PauseState.PauseScheduled(
24 currentPeriodEnd = subscriptionStatus.expiryTime,
25 autoResumeTime = subscriptionStatus.autoResumeTime!!
26 )
27 } else {
28 PauseState.NotPaused
29 }
30 }
31 "SUBSCRIPTION_STATE_PAUSED" -> {
32 PauseState.ActivelyPaused(
33 autoResumeTime = subscriptionStatus.autoResumeTime!!
34 )
35 }
36 else -> PauseState.NotPaused
37 }
38 }
39}
40
41// In your Activity or ViewModel
42fun updatePauseUI() {
43 lifecycleScope.launch {
44 when (val pauseState = pauseStateManager.checkPauseState(userId)) {
45 is PauseState.NotPaused -> {
46 // Normal subscription UI
47 showPremiumContent()
48 }
49 is PauseState.PauseScheduled -> {
50 // User still has access, but pause is coming
51 showPremiumContent()
52 showPauseScheduledBanner(
53 message = "Your subscription will pause on ${formatDate(pauseState.currentPeriodEnd)}",
54 resumeDate = pauseState.autoResumeTime
55 )
56 }
57 is PauseState.ActivelyPaused -> {
58 // No access during pause
59 showPausedStateUI(
60 message = "Your subscription is paused",
61 resumeDate = pauseState.autoResumeTime,
62 onResumeEarlyClick = { openPlayStoreSubscriptionSettings() }
63 )
64 }
65 }
66 }
67}
68
69private fun openPlayStoreSubscriptionSettings() {
70 // Deep link to Play Store subscription management
71 val intent = Intent(Intent.ACTION_VIEW).apply {
72 data = Uri.parse(
73 "<https://play.google.com/store/account/subscriptions?sku=$productId&package=$packageName>"
74 )
75 setPackage("com.android.vending")
76 }
77 startActivity(intent)
78}
The complexity here involves distinguishing between a scheduled pause (where users still have access) and an active pause (where access is revoked). You also need to handle the case where users cancel their scheduled pause, and provide a way for users to resume early if they choose.
With RevenueCat, pause state management becomes straightforward:
1fun checkPauseState() {
2 Purchases.sharedInstance.getCustomerInfoWith { customerInfo ->
3 val entitlement = customerInfo.entitlements["premium"]
4
5 when {
6 entitlement?.isActive == true -> {
7 showPremiumContent()
8
9 // Check if pause is scheduled (will show in periodType)
10 entitlement.expirationDate?.let { expiration ->
11 if (entitlement.willRenew == false && entitlement.unsubscribeDetectedAt == null) {
12 // Subscription won't renew but wasn't canceled - likely paused
13 showPauseScheduledBanner(expiration)
14 }
15 }
16 }
17 entitlement != null && !entitlement.isActive -> {
18 // Could be paused, expired, or other non-active state
19 // RevenueCat's managementURL lets users manage their pause
20 showPausedOrExpiredUI(
21 managementUrl = customerInfo.managementURL
22 )
23 }
24 else -> {
25 showSubscriptionOffers()
26 }
27 }
28 }
29}
RevenueCat automatically tracks pause state through its server-side RTDN processing. The managementURL property provides a direct link to Google Play’s subscription settings where users can view their pause status, cancel a scheduled pause, or resume early. This eliminates the need to build custom deep links or track pause scheduling on your backend.
Additionally, RevenueCat’s webhooks notify your server of pause events in a normalized format, making it easy to trigger pause-related communications like “We miss you!” emails or special offers to encourage early resumption.
Cancellations and expirations
When a user decides to cancel their subscription, the lifecycle enters its terminal phase, but cancellation doesn’t mean immediate loss of access.
The cancellation grace period
Upon cancellation, users retain access until the end of their current billing period. The subscription enters SUBSCRIPTION_STATE_CANCELED, and the expiryTime indicates when access will be revoked.
1fun handleCancellation(notification: SubscriptionNotification) {
2 val subscription = getSubscriptionDetails(notification.purchaseToken)
3
4 val canceledContext = subscription.canceledStateContext
5 val userReason = canceledContext?.userInitiatedCancellation?.cancelSurveyResult
6
7 // Log cancellation reason for analytics
8 analytics.logCancellation(
9 reason = userReason,
10 remainingAccessTime = subscription.lineItems[0].expiryTime
11 )
12
13 // User still has access - don't revoke yet
14 // Just update UI to show cancellation status
15 showCancellationStatus(subscription.lineItems[0].expiryTime)
16}
Understanding cancellation reasons
The canceledStateContext field provides valuable information about why the subscription was canceled:
- User voluntarily canceled (check
userInitiatedCancellationfor the survey response) - Developer canceled via API
- Subscription was replaced (upgrade/downgrade)
- Payment failed and all recovery mechanisms were exhausted
- Google canceled due to policy violations
This information is valuable for understanding churn patterns and improving retention strategies.
Expiration: end of the line
When the expiryTime passes for a canceled subscription, or when account hold ends without recovery, the subscription expires. You receive a SUBSCRIPTION_EXPIRED notification, and the state becomes SUBSCRIPTION_STATE_EXPIRED.
At this point, you should:
- Revoke all entitlements associated with the subscription
- Mark the purchase token as invalid in your database
- Optionally, offer win-back promotions to the user
Restore and resubscribe: bringing users back
Google Play provides two mechanisms for users to return to a subscription they previously had: restore and resubscribe.
Restore: before expiration
If a user cancels but then changes their mind before the subscription expires, they can restore it. This uses the same purchase token and continues the subscription as if it was never canceled.
1fun handleRestore(notification: SubscriptionNotification) {
2 // SUBSCRIPTION_RESTARTED notification
3 // Same purchase token, cancellation fields cleared
4
5 val subscription = getSubscriptionDetails(notification.purchaseToken)
6
7 // Verify it's now active again
8 if (subscription.subscriptionState == "SUBSCRIPTION_STATE_ACTIVE") {
9 // Update your database - subscription is back
10 subscriptionRepository.markRestored(notification.purchaseToken)
11
12 // No acknowledgment needed - same purchase
13 }
14}
Your app receives a SUBSCRIPTION_RESTARTED notification, and the subscription returns to normal active status. Importantly, you don’t need to acknowledge restored subscriptions because you already acknowledged the original purchase.
Resubscribe: after expiration
If the subscription has already expired, users can resubscribe, but this is treated as a new purchase with a new purchase token. You’ll receive a SUBSCRIPTION_PURCHASED notification and must acknowledge within three days.
The resubscribe response includes helpful fields for linking the new subscription to the user’s existing account:
1fun handleResubscribe(notification: SubscriptionNotification) {
2 val subscription = getSubscriptionDetails(notification.purchaseToken)
3
4 // Check for previous subscription context
5 val previousContext = subscription.outOfAppPurchaseContext
6 val previousToken = previousContext?.expiredPurchaseToken
7 val previousAccountId = previousContext?.expiredExternalAccountIdentifiers
8
9 if (previousAccountId != null) {
10 // Link new subscription to existing user account
11 userRepository.linkSubscription(previousAccountId, notification.purchaseToken)
12 }
13
14 // Must acknowledge new purchase
15 acknowledgePurchase(notification.purchaseToken)
16}
Upgrades, downgrades, and plan changes
When users change their subscription plan, whether upgrading to a more expensive option or downgrading to save money, it creates a new subscription while invalidating the old one.
The linked purchase token
Plan changes generate a new purchase token, but the response includes a linkedPurchaseToken field pointing to the previous subscription:
1fun handlePlanChange(notification: SubscriptionNotification) {
2 val newSubscription = getSubscriptionDetails(notification.purchaseToken)
3 val oldToken = newSubscription.linkedPurchaseToken
4
5 if (oldToken != null) {
6 // Find user by old purchase token
7 val user = userRepository.findByPurchaseToken(oldToken)
8
9 // Update to new purchase token
10 userRepository.updatePurchaseToken(user, notification.purchaseToken)
11
12 // Invalidate old token
13 subscriptionRepository.invalidate(oldToken)
14 }
15
16 // Must acknowledge new purchase
17 acknowledgePurchase(notification.purchaseToken)
18}
Proration modes
When implementing plan changes in your app, you can control how billing is handled using different proration modes:
| Mode | Behavior |
| IMMEDIATE_WITH_TIME_PRORATION | User is credited/charged immediately, billing date unchanged |
| IMMEDIATE_AND_CHARGE_PRORATED_PRICE | User charged prorated amount immediately |
| IMMEDIATE_AND_CHARGE_FULL_PRICE | User charged full new plan price immediately |
| DEFERRED | Change takes effect at next renewal |
The choice of proration mode affects user experience and revenue recognition, so consider your business requirements carefully.
Revocations and refunds
Sometimes subscriptions end abruptly due to revocation or refund, bypassing the normal cancellation flow.
When revocation occurs
You receive a SUBSCRIPTION_REVOKED notification when:
- You revoke the subscription via the Developer API
- A chargeback occurs
- Google revokes due to policy violations
Revocations are immediate, the subscription jumps straight to SUBSCRIPTION_STATE_EXPIRED, and you should revoke access immediately:
1fun handleRevocation(notification: SubscriptionNotification) {
2 // Immediate access revocation
3 val purchaseToken = notification.purchaseToken
4
5 // Revoke entitlement immediately
6 entitlementRepository.revoke(purchaseToken)
7
8 // Mark subscription as revoked
9 subscriptionRepository.markRevoked(purchaseToken)
10
11 // Log for fraud detection if this is a chargeback
12 if (notification.notificationType == "SUBSCRIPTION_REVOKED") {
13 fraudDetection.logChargeback(purchaseToken)
14 }
15}
Prepaid subscriptions: a different lifecycle
While this article focuses primarily on auto-renewing subscriptions, it’s worth understanding how prepaid plans differ. Prepaid subscriptions don’t automatically renew; users explicitly purchase additional times.
Key differences for prepaid plans
| Aspect | Auto-Renewing | Prepaid |
| Renewal | Automatic | User-initiated top-up |
| Grace Period | Yes | No |
| Account Hold | Yes | No |
| Pausing | Yes (if enabled) | No |
| States | All states | Only Active, Pending, Expired |
Prepaid acknowledgment timing
Prepaid plans have stricter acknowledgment requirements:
- Plans ≥ 1 week: acknowledge within three days
- Plans < 1 week: acknowledge within half the plan duration
For a three–day prepaid plan, you must acknowledge within one and a half days, or the user receives a refund.
How RevenueCat simplifies lifecycle management
Managing all these lifecycle states, notifications, and edge cases requires sophisticated backend infrastructure and careful implementation. This is where RevenueCat provides substantial value by handling most of this complexity automatically.
Automatic state management
RevenueCat maintains subscription state in real-time, processing Google Play’s RTDN notifications on your behalf. Instead of building infrastructure to receive, validate, and process notifications, you simply query RevenueCat for the current customer state:
1fun checkAccess() {
2 Purchases.sharedInstance.getCustomerInfoWith { customerInfo ->
3 // RevenueCat has already processed all lifecycle events
4 val isPremium = customerInfo.entitlements["premium"]?.isActive == true
5
6 if (isPremium) {
7 enablePremiumFeatures()
8 } else {
9 showSubscriptionOptions()
10 }
11 }
12}
The CustomerInfo object reflects the current state of all subscriptions, including:
- Active entitlements
- Expiration dates
- Whether the user is in a grace period
- Billing issues that need attention
- Management URL for subscription settings
Handling grace periods and billing issues
RevenueCat’s CustomerInfo includes a billingIssueDetectedAt timestamp when a subscription has payment problems. You can use this to show appropriate messaging:
1fun checkBillingStatus(customerInfo: CustomerInfo) {
2 val entitlement = customerInfo.entitlements["premium"]
3
4 if (entitlement?.billingIssueDetectedAt != null) {
5 // User has a billing issue - show recovery UI
6 showBillingRecoveryMessage(
7 managementUrl = customerInfo.managementURL
8 )
9 }
10}
RevenueCat also provides webhooks that notify your server of subscription events in a normalized format, making server-side integration much simpler than processing raw RTDN notifications.
Cross-platform subscription state
One of RevenueCat’s most convenient features is maintaining subscription state across platforms. If a user subscribes on Android and later opens your iOS app, their subscription status is automatically recognized. This is particularly valuable for lifecycle events, a subscription that enters grace period on Android will be reflected in the iOS app’s CustomerInfo without any additional implementation.
Revenue recovery
RevenueCat’s Billing Alerts feature can automatically attempt to recover failed payments by:
- Sending customizable email notifications to users with billing issues
- Prompting users to update payment methods at optimal times
- Tracking recovery rates and providing analytics
This automates much of the grace period and account hold handling that would otherwise require custom implementation.
Analytics and insights
Understanding your subscription lifecycle patterns is crucial for optimization. RevenueCat provides detailed analytics including:
- Churn analysis by cancellation reason
- Grace period and account hold recovery rates
- Subscription duration and renewal patterns
- Revenue metrics across lifecycle stages
These insights help you identify where users are dropping off and opportunities to improve retention.
Best practices for lifecycle management
Based on the lifecycle stages we’ve covered, here are key practices to implement:
Always verify on your backend
Never trust the client-side subscription state alone. Your backend should:
- Process RTDN notifications (or use RevenueCat’s webhooks)
- Verify purchases using the Google Play Developer API
- Maintain authoritative subscription state
Handle grace periods proactively
Users in grace periods are at high risk of churning. Implement multiple touchpoints:
- In-app messaging using Google’s API
- Push notifications reminding users to update payment
- Email campaigns for users not opening the app
Make cancellation reversible
Since users retain access until their billing period ends, make it easy to restore:
- Show clear ‘Resume subscription’ options
- Don’t punish users who explore cancellation
- Consider exit surveys but don’t make them mandatory
Plan for edge cases
Real-world subscription management involves many edge cases:
- Users switching devices mid-subscription
- Multiple purchases from the same user
- Refunds and chargebacks
- Subscription transfers between accounts
Build your system to handle these gracefully, or use a service like RevenueCat that handles them automatically.
Summary
Google Play’s subscription lifecycle is comprehensive but complex. From the initial purchase through renewals, grace periods, account holds, pauses, cancellations, and expirations, each state requires specific handling to ensure users receive correct entitlements while your business captures all possible revenue.
The key states to understand are:
- Active state where users have full access
- Grace period where users retain access while you attempt payment recovery
- Account hold where access is suspended pending payment fix
- Paused state where users voluntarily pause their subscription
- Canceled state where users retain access until their paid period ends
- Expired state where access should be revoked
Whether you implement subscription lifecycle management directly or use RevenueCat, understanding these lifecycle stages is essential for building a robust subscription business on Android. The difference between losing a subscriber to involuntary churn and recovering them often comes down to how well you handle grace periods and account holds. The difference between a confusing user experience and a seamless one depends on how gracefully you handle pauses, cancellations, and restorations.
For complete documentation on subscription lifecycle management, refer to the official Android Developer documentation and RevenueCat’s subscription guidance.

