Billing grace period and account hold: recover failed renewals
When a renewal charge fails, neither store cancels the subscription at once. Apple retries for up to 60 days and lets you turn on a billing grace period of 3, 16 or 28 days where the customer keeps access. Google Play runs a grace period, then an account hold where access stops, and by default the two together last 60 days. Customers who lose a subscription to a failed card are called involuntary churn, and a short setup recovers many of them.
This post explains each store's timeline, the notifications that tell your server what happened, how to keep access right in your app, and how to test it. Sources are Apple, Google and RevenueCat docs, checked in October 2026.
The short answer#
- Apple. A failed renewal puts the subscription in billing retry, and the App Store tries to collect for up to 60 days (Apple).
- Apple grace period. Optional, set per app, 3, 16 or 28 days. A weekly subscription gets 6 days at most. The customer keeps full access during it (Apple).
- Google grace period. On by default for auto-renewing base plans. You can change its length or turn it off (Android Developers).
- Google account hold. Starts after the grace period and removes access. The default length is 60 days minus the grace period (Google).
- Recovery. Fix inside Apple's grace period and nothing is interrupted. Fix later and the billing date moves to the recovery date. Google behaves the same way for account hold.
What is involuntary churn?#
Apple defines it as subscribers who do not intend to leave but whose subscription fails to renew, usually because of a billing issue. Because it has nothing to do with customer satisfaction, Apple suggests designing for it with a grace period, in-app prompts, or both (Apple). Common causes are an expired card or a low balance.
How long does each store keep trying?#
| App Store | Google Play | |
|---|---|---|
| Access during grace | Yes, if you turn grace on | Yes, on by default |
| Grace length | 3, 16 or 28 days (6 for weekly) | You choose. A 0-day setting still waits at least 1 day, silently |
| After grace | Billing retry continues, no access | Account hold, no access |
| Total recovery window | Up to 60 days | Grace plus hold. Default hold is 60 days minus grace |
| Billing date after recovery in grace | Unchanged | Unchanged |
| Billing date after later recovery | Moves to the recovery date | Moves to the recovery date |
| System prompt to the user | iOS 16.4 and later show a payment sheet at app launch | A snackbar when you call the In-App Messaging API |
Sources: Apple, Apple App Store Connect help, Apple subscriptions, Android Developers and Google Play Help.
Two Apple details worth knowing. The grace period is applied when the billing error happens and cannot change for that customer afterward. And it does not apply to monthly subscriptions with a 12-month commitment. Apple also notes that if a subscription is recovered within 60 days, the days of paid service resume from the recovery date, which protects the one-year count for the 85% proceeds rate (Apple).
Two Google details. The default account hold was raised on December 1, 2025 from 30 days to an automatically calculated value, and Google tells developers to read the state from the API instead of assuming a static number (Google Play Help). And a "silent" grace period of up to a day keeps the subscription in the active state with no notification, so a notification may not arrive on the day the charge fails.
How do I turn on the grace period?#
On Apple. In App Store Connect open your app, then Subscriptions, then Billing Grace Period, and click Set Up Billing Grace Period. Pick 3, 16 or 28 days. It applies to every subscription in the app, not to single products (Apple). Apple recommends turning it on in the sandbox first, testing, then turning on production. You can apply it to all renewals, or only to existing paid renewals (Apple).
On Google Play. Each auto-renewing base plan has Grace period and Account hold settings in Play Console. Google warns that a length below the default can reduce the number of recovered subscriptions (Android Developers).
Which notifications tell my server what happened?#
| State | App Store Server Notifications V2 | Google Play real-time notification | RevenueDot status |
|---|---|---|---|
| Renewal fails, grace on | DID_FAIL_TO_RENEW with subtype GRACE_PERIOD |
SUBSCRIPTION_IN_GRACE_PERIOD |
in_grace_period |
| Grace ends, still retrying | GRACE_PERIOD_EXPIRED |
SUBSCRIPTION_ON_HOLD |
in_billing_retry |
| Payment recovered | DID_RENEW with subtype BILLING_RECOVERY |
SUBSCRIPTION_RENEWED or SUBSCRIPTION_RECOVERED |
active |
| Recovery fails | EXPIRED with subtype BILLING_RETRY |
SUBSCRIPTION_EXPIRED |
expired |
Apple's notification list is on its notificationType page. Google's states and notifications are in its subscription lifecycle guide, which also says to call purchases.subscriptionsv2.get when a notification arrives, because it is the source of truth. RevenueDot's statuses are from its subscription lifecycle reference.
How should my app treat a customer in a billing problem?#
Treat grace as paid access and retry or hold as no access. Apple's Transaction.currentEntitlements includes subscriptions in the subscribed and inGracePeriod states, and leaves out inBillingRetryPeriod, expired and revoked (Apple). On Android, queryPurchasesAsync still returns purchases in grace and stops returning them during account hold (Android Developers). A server-backed SDK gives you the same rule through entitlements. RevenueCat's SDK keeps a subscription in grace active, and it says the subscription is considered cancelled but not expired until the grace period ends (RevenueCat).
Then ask the customer to fix the payment method. With StoreKit 2 you can read the grace state and show a banner:
let statuses = try await product.subscription?.status ?? []
for status in statuses {
guard case .verified(let renewal) = status.renewalInfo else { continue }
if status.state == .inGracePeriod, let end = renewal.gracePeriodExpirationDate {
showPaymentBanner(until: end) // link to https://apps.apple.com/account/billing
}
}Apple's own deep link to the billing page is https://apps.apple.com/account/billing on iOS and macOS. Starting in iOS 16.4, a failed renewal also makes the system show a payment sheet when the app launches, and you can delay or suppress it with StoreKit messages (Apple). On Android, call the In-App Messaging API when the app opens, and link to https://play.google.com/store/account/subscriptions?sku=<product>&package=<package> (Android Developers).
How do I test grace and retry?#
- Xcode. In a StoreKit test session, set
shouldEnterBillingRetryOnRenewalandbillingGracePeriodIsEnabledtotrue, and callresolveIssueForTransaction(identifier:)to simulate the fix (Apple). - Google Play. Play Billing Lab, with a license tester account, moves a test subscription into grace or account hold in one click. Test grace lasts 5 minutes and test account hold 10 minutes (Android Developers).
- No store. RevenueDot's Test Store has a
billing_issuescenario with 7 days of grace. See testing in-app purchases.
Do it with RevenueDot#
RevenueDot reads each store's state and gives you one status per subscription and one webhook event.
- Connect the store: the App Store guide (In-App Purchase key and notification URL) and the Google Play guide (service account and Pub/Sub push). Without notifications you learn about a failure only when the app next opens.
- Add a webhook. A failed renewal sends
BILLING_ISSUEwithgrace_period_expiration_at_ms, thenCANCELLATIONbecause the subscription will not renew, as in the Test Store'sbilling_issuescenario. A recovery sendsRENEWAL, and a lost customer getsEXPIRATION(webhook events). - Send
BILLING_ISSUEto your email or push tool. RevenueDot's integrations, such as Braze, Customer.io and OneSignal, get every event a webhook gets. - Check one customer in the API.
statusisin_grace_periodorin_billing_retry, andgives_accesssays whether to give access:
curl -s "https://api.revenuedot.app/v2/projects/$PROJECT_ID/customers/user_1/subscriptions" \
-H "Authorization: Bearer $SECRET_KEY"
# ... "status": "in_grace_period", "gives_access": true, "auto_renewal_status": "will_renew" ...Track the result on the churn rate chart and the subscription status chart. The stores' paths are tested against mocked Apple and Google APIs, and a real run in each sandbox is still pending, so test with real sandbox accounts first.
Start free on RevenueDot Cloud (free up to $10,000 monthly tracked revenue). See also the webhooks feature page.
FAQ#
Should I enable a billing grace period?#
Yes. Apple says a grace period gives subscribers uninterrupted service and avoids losing days of paid service and revenue if Apple recovers the payment in time (Apple). On Google Play it is on by default, and shortening it can reduce recovered subscriptions.
How long does Apple retry a failed renewal?#
Up to 60 days. The grace period, if you enable it, sits at the start of that window (Apple).
What is account hold on Google Play?#
It is the period after any grace period when Google still retries the payment but the user must not have access. If the hold ends with the payment unresolved, the subscription expires (Google Play Help).
Does a recovered subscriber keep the original renewal date?#
If the payment is fixed inside the grace period, yes on both stores. If it is fixed after the grace period, the billing date moves to the date of recovery (Apple, Android Developers).
Can I email customers whose payment failed?#
Yes. Use the billing-issue event from your server, since both stores also show their own prompts. RevenueDot sends BILLING_ISSUE to webhooks and email tools, and the event includes when grace ends.
About RevenueDot. RevenueDot is an open-source (AGPL-3.0) backend for in-app purchases and subscriptions that works with the RevenueCat SDK. Start free on RevenueDot Cloud, free up to $10,000 in monthly tracked revenue, or self-host it with Docker and Postgres. Point the SDK's proxy URL at RevenueDot and keep your app code, your offerings and your customers. Read the quickstart or the code on GitHub.