Back to the RevenueCat homepage
RevenueCat SDKGoogle Play Billing

Chapter 12: Payment Recovery

Handling payment recovery from scratch means detecting grace periods, building a checkGracePeriodStatus() function, displaying in-app messages at the right moments, understanding the silent grace period, and responding to SUBSCRIPTION_IN_GRACE_PERIOD and SUBSCRIPTION_ON_HOLD RTDNs on the backend.

With RevenueCat, two of these concerns require code from you. The rest are automatic.

In-App Messages: Automatic by Default

RevenueCat shows Google Play's in-app payment recovery messages automatically. The SDK calls showInAppMessages() when BillingClient connects, which displays the snackbar prompting users to fix their payment method during grace period and account hold.

You do not call any in-app messaging API. This is on by default with:

kotlin
PurchasesConfiguration.Builder(context, apiKey)
    .showInAppMessagesAutomatically(true) // this is the default
    .build()

If you want to control when the message appears, for example, only on certain screens, disable the automatic behavior and call it manually:

kotlin
PurchasesConfiguration.Builder(context, apiKey)
    .showInAppMessagesAutomatically(false)
    .build()

// Call where appropriate
Purchases.sharedInstance.showInAppMessagesIfNeeded(activity)

Detecting Payment Issues in Your UI

To show your own payment recovery UI (a banner, a dialog, or a dedicated screen), check EntitlementInfo.billingIssueDetectedAt:

kotlin
val entitlement = customerInfo.entitlements["pro_access"]
if (entitlement?.isActive == true &&
    entitlement.billingIssueDetectedAt != null) {
    showPaymentIssueWarning()
}

billingIssueDetectedAt is non-null from the moment RevenueCat receives the SUBSCRIPTION_IN_GRACE_PERIOD RTDN until the payment issue is resolved. This covers both grace period (access retained) and account hold (access revoked). Since isActive is still true during grace period but false during account hold, you can distinguish:

kotlin
val entitlement = customerInfo.entitlements["pro_access"]
when {
    entitlement == null || !entitlement.isActive ->
        showSubscribeScreen()
    entitlement.billingIssueDetectedAt != null && entitlement.isActive ->
        showGracePeriodWarning() // user has access but payment is failing
    entitlement.billingIssueDetectedAt != null && !entitlement.isActive ->
        showAccountHoldScreen() // access revoked, prompt to fix payment
    else ->
        showPremiumContent()
}

Opening Payment Management

Send users to fix their payment method:

kotlin
customerInfo.managementURL?.let { url ->
    startActivity(Intent(Intent.ACTION_VIEW, url))
}

managementURL is a Uri? pointing to the subscription management page in Google Play. RevenueCat populates this automatically.

Backend: Webhooks Handle the Rest

On your backend, the BILLING_ISSUE webhook event fires when a subscription enters grace period or account hold. The event includes the app_user_id and entitlement_ids, so you can flag the user's account to show payment recovery prompts.

json
{
  "type": "BILLING_ISSUE",
  "app_user_id": "user_12345",
  "entitlement_ids": ["pro_access"],
  "expiration_at_ms": 1702592000000
}

Your webhook handler flags the user's account. No RTDN decoding, no state machine update needed.

Prefer building from scratch?

The Google Play Billing Handbook covers the same topics with raw BillingClient, Developer API, and RTDNs.

Related chapters

  • Chapter 11: Subscription States

    Seven complex subscription states are resolved to just one simple boolean check: isActive.

    Learn more
  • Chapter 13: Cancellations, Pauses, and Winback

    Read unsubscribeDetectedAt and expirationDate. That's your entire cancellation handler, done.

    Learn more
  • Chapter 10: Webhooks

    One endpoint with normalized JSON events. No Cloud Pub/Sub configuration, no base64 decoding.

    Learn more
Payment Recovery | RevenueCat