โ† All articles

iOS Billing Grace Period and Billing Retry, Explained

When an iOS subscription renewal fails, billing retry and the billing grace period keep customers in access while Apple retries. How to handle both.

iOS Billing Grace Period and Billing Retry, Explained

When an auto-renewable subscription tries to renew and the payment fails, the subscription does not end on the spot. The App Store moves it into a billing retry state and keeps attempting the charge for up to 60 days. If you enable the billing grace period, the customer also keeps full access for a short window while those retries happen, so a temporary card problem does not instantly lock a paying customer out of your app. Handling these two states well is the difference between recovering a failed renewal and losing a subscriber who never meant to churn.

Most subscription cancellations are not people deciding to leave. They are payments that quietly failed, a category called involuntary churn. This post explains what billing retry and the grace period are, how to turn the grace period on, how to read both states from StoreKit 2 and RevenueCat, and what to show the customer so the renewal recovers instead of lapsing.

On this page

Billing retry and grace period, defined

These are two related things that happen after a renewal charge fails:

  • Billing retry is automatic and always on. When a renewal payment fails, the App Store keeps retrying the charge for up to 60 days before it finally marks the subscription expired. During plain billing retry (with no grace period), the customer's access has already lapsed while Apple keeps trying.
  • Billing grace period is opt-in. When you enable it, the customer retains access for a set window at the start of that retry process. Apple sets the length: 16 days for subscriptions with a duration of a week or longer, and a shorter window for weekly and shorter subscriptions. If the charge succeeds during the grace period, the customer never experiences an interruption at all.

The practical takeaway: billing retry recovers the payment, but the grace period is what protects the experience. Without a grace period, a subscriber whose card expired is locked out immediately even though Apple is actively trying to charge them and will likely succeed within a day or two. For the surrounding lifecycle, see how an iOS subscription works end to end.

Turning on the billing grace period

The grace period is an app-level subscription setting in App Store Connect, and it is off by default. Under your app's subscription configuration, find the billing grace period option and enable it. There is essentially no downside for a normal paid app: you are giving customers who already intend to pay a few days of continued access while their renewal recovers, which almost always converts back to a paid period.

Enable it before you write any client code, because your access logic below depends on the inGracePeriod state existing. If the grace period is off, a failed renewal skips straight to billing retry with no access, and you lose the window to recover the customer gracefully.

Reading the states in StoreKit 2

StoreKit 2 exposes both states through the subscription's renewal state. Map them to a single access decision your UI can act on:

import StoreKit
 
enum AccessState { case active, gracePeriod, billingRetry, expired }
 
func accessState(inGroup groupID: String) async -> AccessState {
    guard let statuses = try? await Product.SubscriptionInfo.status(for: groupID),
          let status = statuses.first else {
        return .expired
    }
 
    switch status.state {
    case .subscribed:
        return .active
    case .inGracePeriod:
        // Payment failed, but the grace period keeps access on. Still entitled.
        return .gracePeriod
    case .inBillingRetryPeriod:
        // Grace period is over (or was never enabled). Access has lapsed while
        // the App Store keeps retrying the charge.
        return .billingRetry
    default:
        return .expired
    }
}

The renewal info carries the details you need for messaging, including whether a retry is in progress and when the grace period ends:

guard case .verified(let renewal) = status.renewalInfo else { return }
 
if renewal.isInBillingRetry {
    // A renewal charge is currently failing.
}
 
if let graceEnd = renewal.gracePeriodExpirationDate {
    // Access stays on until this date. Use it to show a deadline in the prompt.
}

Keeping access during the grace period

The single most important rule: grant entitlement when the state is .subscribed or .inGracePeriod. A customer in the grace period is still a paying customer whose renewal is being retried, so cutting them off defeats the entire purpose of enabling it.

let state = await accessState(inGroup: "group.pro")
let hasAccess = (state == .active || state == .gracePeriod)

Do not grant access during .inBillingRetryPeriod. By that point the grace window has ended (or was never enabled) and the customer genuinely has no active subscription, though Apple may still recover it. When it does recover, the state flips back to .subscribed on its own and your access check starts returning true again with no purchase call from you.

Handling billing issues with RevenueCat

RevenueCat folds this into the entitlement so you do not track raw states yourself. During the grace period the entitlement stays active, and RevenueCat flags that a payment problem exists through billingIssueDetectedAt:

import RevenueCat
 
let info = try await Purchases.shared.customerInfo()
if let pro = info.entitlements["pro"], pro.isActive {
    // `isActive` already includes the grace period, so access stays on.
    if pro.billingIssueDetectedAt != nil {
        // Still entitled, but a renewal is failing. Nudge them to fix payment.
    }
}

This is the cleanest part of using RevenueCat for subscriptions: your gate stays a single entitlements["pro"]?.isActive check that already respects the grace period, and billingIssueDetectedAt is the one extra signal you read to decide whether to show a soft "update your payment method" prompt. The underlying setup is covered in adding subscriptions with RevenueCat, and how tiers and switching interact lives in subscription groups, upgrades, and downgrades.

What to show the customer

The grace period only recovers renewals if the customer notices and fixes the problem, so the in-app prompt matters:

  • Keep them in the app. During the grace period, do not gate features. Show a dismissible banner, not a wall.
  • Say what happened plainly. "There is a problem with your payment method. Update it before [grace end date] to keep your subscription." Use gracePeriodExpirationDate for the deadline.
  • Send them to the right place. The customer fixes billing in the App Store account settings, not inside your app. You can present the manage-subscriptions sheet with StoreKit's AppStore.showManageSubscriptions(in:) or deep-link to the account's billing settings.
  • Escalate only after access actually lapses. Once the state is billing retry (access gone), a stronger prompt is fair, since the subscription is genuinely inactive until the charge recovers.

Common gotchas

  • The grace period is off until you enable it. A brand-new app has no grace period, so every failed renewal locks customers out immediately. Turn it on in App Store Connect.
  • Grace period means still-entitled. If your access check only looks for .subscribed, you will wrongly lock out paying customers whose renewal is recovering. Include .inGracePeriod.
  • Billing retry is not access. Do not keep features unlocked during .inBillingRetryPeriod. That state means the grace window is over and the subscription is currently inactive.
  • Do not force a new purchase. A recovering renewal resolves itself when the retry succeeds. Prompting the customer to buy again can create a duplicate subscription.
  • Server notifications mirror this. App Store Server Notifications report the same transitions (a failed renewal, entering and exiting the grace period) if you run a backend, but the client-side state above is enough for most indie apps.

FAQ

What is the difference between billing retry and a billing grace period?

Billing retry is the App Store automatically re-attempting a failed renewal charge for up to 60 days, and it is always on. The billing grace period is an opt-in window at the start of that process during which the customer keeps full access while the retries happen.

How long is the iOS billing grace period?

Apple sets the length based on the subscription duration: 16 days for subscriptions of a week or longer, and a shorter window for weekly and shorter subscriptions. You enable it per app in App Store Connect; you do not choose an arbitrary number of days.

Should I keep the subscription features unlocked during the grace period?

Yes. A customer in the grace period is still a paying subscriber whose renewal is being retried, so grant entitlement for both .subscribed and .inGracePeriod. Only revoke access once the state becomes billing retry, when the subscription is genuinely inactive.

Does the customer get charged again automatically?

Yes. The App Store keeps retrying the original renewal for up to 60 days. If a retry succeeds, the subscription returns to active on its own, with no new purchase from your app. You should never trigger a fresh purchase to "fix" it.

How do I detect a billing issue with RevenueCat?

Check the entitlement. During the grace period entitlements["pro"]?.isActive stays true, and billingIssueDetectedAt is non-nil when a renewal is failing. Use that flag to show a soft prompt asking the customer to update their payment method.

Why did a subscriber lose access even though Apple is still retrying?

Because you did not enable the billing grace period, or your access logic ignores .inGracePeriod. Without a grace period, a failed renewal goes straight to billing retry, where access has already lapsed even though the App Store keeps trying to charge.


Recovering failed renewals only works if your app checks the entitlement, not a raw product flag, so grace-period customers stay unlocked automatically. That is exactly how Spaceport wires it. The generated SwiftUI project ships with RevenueCat and a subscription manager whose access checks read the entitlement (which already includes the grace period), plus the paywall and a StoreKit configuration file for testing, all created against subscription products Spaceport sets up and prices in App Store Connect through the API. So the involuntary-churn handling described here is the default behavior, not something you bolt on later. And when you are lining up a waitlist and launch-day audience for the app, our sister tool Lighthouse covers that side.

From an indie iOS dev, for indie iOS devs.

Read more at spaceport.build

Community appsJoin Discord