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:

FieldValue for new purchase
subscriptionStateSUBSCRIPTION_STATE_ACTIVE
acknowledgementStateACKNOWLEDGEMENT_STATE_PENDING
autoRenewEnabledtrue
expiryTimeNext 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 periodAvailable grace period options
Weekly3, 7 days
Monthly7, 14, 30 days
Annual7, 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_RENEWED if the retry succeeded
  • SUBSCRIPTION_ON_HOLD if account hold is enabled
  • SUBSCRIPTION_CANCELED if the user canceled during this time
  • SUBSCRIPTION_EXPIRED if 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 expiryTime is 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 PeriodAvailable Pause Durations
Weekly1, 2, 3, 4 weeks
Monthly1, 2, 3 months
Three-month1, 2, 3 months
Six-month1, 2, 3 months
AnnualNot 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:

  1. User initiates pause: SUBSCRIPTION_PAUSE_SCHEDULE_CHANGED notification
    • State remains SUBSCRIPTION_STATE_ACTIVE
    • User retains access until current billing period ends
  2. Pause takes effect: SUBSCRIPTION_PAUSED notification
    • State becomes SUBSCRIPTION_STATE_PAUSED
    • User loses access
    • PausedStateContext contains the expected resume date
  3. Subscription resumes: SUBSCRIPTION_RECOVERED notification
    • 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 userInitiatedCancellation for 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:

ModeBehavior
IMMEDIATE_WITH_TIME_PRORATIONUser is credited/charged immediately, billing date unchanged
IMMEDIATE_AND_CHARGE_PRORATED_PRICEUser charged prorated amount immediately
IMMEDIATE_AND_CHARGE_FULL_PRICEUser charged full new plan price immediately
DEFERREDChange 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

AspectAuto-RenewingPrepaid
RenewalAutomaticUser-initiated top-up
Grace PeriodYesNo
Account HoldYesNo
PausingYes (if enabled)No
StatesAll statesOnly 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.