The Involuntary Churn Problem
Not every cancellation is a customer who wanted to leave. A large share are simply failed payments: an expired card, an insufficient balance, a bank decline. The user never chose to cancel, but without a recovery mechanism they silently lose access and you lose the revenue.
RevenueCat's State of Subscription Apps 2026 found that about a third of Google Play subscription cancellations are involuntary billing failures (roughly double the App Store's share). The report's phrasing: fixing billing is one of the clearest growth levers available. This codelab shows you how.
You will:
- Enable billing grace periods in App Store Connect and Google Play so access continues while the store retries the card.
- Detect an active subscription that is in billing retry with RevenueCat (iOS, Android, React Native).
- Let users fix their payment method with Customer Center or the store's
managementURL. - React to the BILLING_ISSUE webhook to re-engage users outside the app.
Voluntary vs Involuntary Churn
Two very different events both show up as "a subscription ended." Treat them differently.
| Type | Cause | How you recover it |
|---|---|---|
| Voluntary | User tapped cancel | Cancel-flow offers, win-back, value reminders |
| Involuntary | Payment failed at renewal | Grace period, billing retry, prompt to fix payment |
This codelab targets involuntary churn. The recovery chain is: the store keeps retrying the card for a window of time (the grace period), the user keeps access during that window, and your app nudges them to update their payment method before the window closes.
unsubscribeDetectedAt
on the entitlement; an involuntary failure sets billingIssueDetectedAt. You will use the latter in
Step 6. (Voluntary churn is covered in a separate Customer Center codelab.)
Enable Grace Period in App Store Connect
A billing grace period lets a subscriber keep access while Apple retries a failed renewal, so there is no interruption to their service or your revenue if Apple recovers the payment in time.
- In App Store Connect, open your app and click Subscriptions in the sidebar.
- In the Billing Grace Period section, click Set Up Billing Grace Period.
- Choose a duration: 3, 16, or 28 days (it applies across all of the app's subscriptions).
- Choose the renewal types (all renewals, or only paid-to-paid) and the environments (sandbox only, or production and sandbox).
- Click Confirm.
Enable Grace Period and Account Hold in Google Play
Google Play has two recovery phases. During the grace period the user keeps access while Google retries the card. If that fails, the subscription moves to account hold, where the user loses access while Google keeps trying. Recover within either phase and the subscription continues seamlessly.
- In Google Play Console, go to Monetize with Play → Products → Subscriptions.
- Open a subscription and select its auto-renewing base plan. Grace period and account hold are configured per base plan.
- Set the grace period (how long users keep access while a renewal payment is unresolved).
- Set the account hold duration (how long Google keeps retrying after access is paused).
How RevenueCat Surfaces a Billing Issue
This is the key insight that makes detection simple. While a subscription is in a grace period, RevenueCat
keeps the entitlement isActive: true (the user still has access) and sets
billingIssueDetectedAt to the time the failure was detected.
So the signal for "paying customer, but their card just failed" is:
entitlement.isActive == true AND entitlement.billingIssueDetectedAt != null
=> active subscriber, in billing retry, nudge them to fix payment
Relevant fields on the entitlement (names are consistent across platforms):
isActive: the customer currently has access. Staystrueduring a grace period.billingIssueDetectedAt: a date/time, set when a payment failure is detected, cleared when payment recovers. Null when there is no issue.willRenew: whether the subscription is set to renew.unsubscribeDetectedAt: set for a voluntary cancel (do not confuse it with a billing issue).
billingIssueDetectedAt set. Once Google moves to account hold,
the entitlement is no longer active, so your normal entitlement check already blocks access.
Detect a Billing Retry In-App
Read the current CustomerInfo and check the pro entitlement for the billing-issue signal.
iOS (Swift)
let info = try await Purchases.shared.customerInfo()
if let pro = info.entitlements["pro"], pro.isActive {
if pro.billingIssueDetectedAt != nil {
// Active subscriber whose payment is failing: show a "fix payment" banner.
showBillingIssueBanner()
}
}
Android (Kotlin)
val info = Purchases.sharedInstance.awaitCustomerInfo()
val pro = info.entitlements["pro"]
if (pro?.isActive == true && pro.billingIssueDetectedAt != null) {
// Active subscriber in billing retry.
showBillingIssueBanner()
}
React Native (TypeScript)
const info = await Purchases.getCustomerInfo();
const pro = info.entitlements.active['pro'];
if (pro && pro.billingIssueDetectedAt != null) {
// Active subscriber in billing retry.
showBillingIssueBanner();
}
CustomerInfo updates so the banner disappears the moment payment recovers. (See the
CustomerInfo listener guide for the
React Native pattern.)
Let Users Fix Payment with Customer Center
Once you have detected the issue, give the user a one-tap way to fix it. There are two routes.
Option A: Customer Center (recommended)
Customer Center is RevenueCat's drop-in, self-service subscription management UI. It routes users to manage their subscription and update payment, and also handles restores, cancellations, and retention offers.
// iOS (SwiftUI) - RevenueCatUI
import RevenueCatUI
struct SettingsView: View {
@State private var showCustomerCenter = false
var body: some View {
Button("Manage Subscription") { showCustomerCenter = true }
.sheet(isPresented: $showCustomerCenter) {
CustomerCenterView()
}
}
}
// React Native - react-native-purchases-ui
import RevenueCatUI from 'react-native-purchases-ui';
await RevenueCatUI.presentCustomerCenter();
On Android, render the CustomerCenter Composable from purchases-ui.
Option B: Open the store's manage page directly
If you are not using Customer Center, send the user straight to the store's manage-subscription page with
customerInfo.managementURL, which RevenueCat fills in for the correct store automatically:
// React Native
import { Linking } from 'react-native';
const info = await Purchases.getCustomerInfo();
if (info.managementURL) {
await Linking.openURL(info.managementURL);
}
On iOS open customerInfo.managementURL with UIApplication.shared.open(_:); on Android
with an ACTION_VIEW intent. See the
Manage Subscriptions guide.
React to Billing Issue Webhooks
The in-app banner only helps users who open the app. To reach the rest, use RevenueCat webhooks on your server to trigger an email or push notification.
BILLING_ISSUE: fired as soon as a payment failure is detected. RevenueCat sends one per issue. When a grace period is configured, it includesgrace_period_expiration_at_ms, the deadline to recover.CANCELLATIONwithcancel_reason: BILLING_ERROR: also fired when the failure is detected.EXPIRATIONwithexpiration_reason: BILLING_ERROR: fired only if the grace period ends without recovery. This is when you finally remove access.RENEWAL: fired when the payment recovers, andbillingIssueDetectedAtclears.
// Your webhook endpoint (Express-style)
app.post('/revenuecat/webhook', (req, res) => {
const event = req.body.event;
switch (event.type) {
case 'BILLING_ISSUE':
// Card failed but access continues. Email "update your payment method"
// with the deadline from event.grace_period_expiration_at_ms.
sendFixPaymentEmail(event.app_user_id, event.grace_period_expiration_at_ms);
break;
case 'RENEWAL':
// Payment recovered. Stop the dunning sequence.
clearDunning(event.app_user_id);
break;
}
res.sendStatus(200);
});
EXPIRATION
(BILLING_ERROR) is deferred until grace ends, giving your dunning sequence time to work. Without one, it fires
immediately alongside BILLING_ISSUE, so access is lost at once. This is exactly why the grace
periods you set up in Steps 3 and 4 matter.
Test in Sandbox
Verify the whole loop before you ship.
- In App Store Connect, enable the grace period for Sandbox (or Production and Sandbox) so you can exercise it with a sandbox tester.
- On Google Play, use a license tester on a closed-testing track to drive a subscription into grace and hold.
- Trigger a renewal failure, then confirm in your app that the
proentitlement is stillisActiveand thatbillingIssueDetectedAtis set, so your banner appears. - Confirm your server received the
BILLING_ISSUEwebhook with agrace_period_expiration_at_ms. - Recover the payment and confirm the banner disappears and a
RENEWALwebhook arrives.
Recap
You built a complete involuntary-churn recovery flow:
- Grace periods in both stores keep paying users in access while the card is retried.
- RevenueCat surfaces the state as active entitlement +
billingIssueDetectedAt, which you detect in-app. - Customer Center (or
managementURL) gives users a one-tap way to update payment. - Webhooks drive out-of-app dunning, with timing that the grace period makes forgiving.
This is one of the highest-leverage changes you can make: you are recovering revenue from customers who never wanted to leave.
Keep going
- Get CustomerInfo and CustomerInfo update listener: the detection building blocks.
- Manage Subscriptions: the managementURL pattern in depth.
- RevenueCat: Billing issues & grace periods and Customer Center.