Grace period, account hold, and billing retry
Involuntary churn starts with a card that declines. Between that decline and a lost subscriber there is a window where the right product behaviour is a nudge, not a lock screen — and then a second window where it is the other way round.
The failed-renewal path
On Google Play, a renewal that cannot be charged moves through up to two recovery phases before it ends. Their lengths are configured per base plan in Play Console, not in Tierux; the grace period and account hold glossary entries summarize Google's defaults. What Tierux controls is what each phase does to the entitlement:
- Grace period. Play sends
SUBSCRIPTION_IN_GRACE_PERIOD. Tierux writes statusgrace_periodwith access on, and appends agrace_periodevent to the timeline. - Account hold. If the payment is still failing when grace ends and account hold is enabled for the base plan, Play sends
SUBSCRIPTION_ON_HOLD. Tierux writes statusaccount_holdwith access off, and appends abilling_retryevent. - Recovery or expiry. If the subscriber fixes their payment method, Play sends
SUBSCRIPTION_RECOVERED; Tierux re-queries Play and writes whatever the fresh state says — normallyactivewith a newexpiresAt. If the window closes unrecovered,SUBSCRIPTION_EXPIREDwritesexpiredwith access off.
The same three states come back from a fresh subscription query: SUBSCRIPTION_STATE_IN_GRACE_PERIOD maps to grace_period with access on, and SUBSCRIPTION_STATE_ON_HOLD maps to account_hold with access off. The full mapping is in chapter 1.
"Billing retry" is an event, not a status
Tierux has no billing_retry status. The six stored statuses are the ones in the entitlement state machine, and a subscription in retry is either grace_period (still entitled) or account_hold (not entitled). billing_retry is the name of the timeline and webhook event recorded when access is suspended for a payment problem — on Google Play, when the hold notification arrives.
So a backend listening to your Tierux webhook sees this sequence on a card that never recovers:
// X-Tierux-Event values, in order grace_period // payment failed, access still on billing_retry // grace ended, access now off (status: account_hold) expired // recovery window closed
A recovery produces no timeline event of its own; the entitlement simply returns to active. The next successful renewal appears as renewal. Webhook delivery is at most once with no retry, so treat these events as prompts and read the entitlement for the current answer.
Grace period and expiresAt
The grace notification is one of the types Tierux applies without re-querying Play. It sets status and access, but leaves the stored expiresAt as it was — typically the end of the period that just failed to renew.
That interacts with the read path. GET /api/v1/entitlements/{userId}/{entitlementId} recomputes active when it reads the record: the stored flag must be on and expiresAt, if set, must be in the future. Because the stored expiry is typically already in the past during grace, a subscriber in grace will usually read back as:
{
"entitlement": "pro",
"active": false,
"expiresAt": "2026-10-01T09:00:00.000Z",
"status": "grace_period",
"winbackPending": false
}That is the expected shape for grace, not a rare edge case. Treat status: grace_period as unlocked in your own gate, or refresh first: a POST /api/v1/entitlements/{userId}/refresh re-queries Play and stores the line item's current expiryTime along with the fresh state. Keep that call out of the hot path — it is one Play round trip per stored purchase — and use it from a "restore purchases" action, a support tool, or a one-off reconciliation.
What to show the subscriber
Choose the message from status, and gate on active except during grace, where active can read false until a refresh. A typical backend handler:
const res = await fetch(`https://tierux.com/api/v1/entitlements/${userId}/pro`, { headers: { Authorization: `Bearer ${process.env.TIERUX_API_KEY}` }, }).then((r) => r.json()); if (res.status === 'grace_period') { showBanner('Payment failed — update your card to keep Pro.'); return true; // stored expiresAt may already be past, so res.active can be false } if (res.status === 'account_hold') showRecovery('Pro is paused until your payment goes through.'); return res.active;
- During grace, keep the feature unlocked and show a non-blocking "update your payment method" prompt that links to Play's subscription management. Locking a paying subscriber out over a card problem turns a billing issue into a product outage.
- During hold, the feature is locked. Say that access is paused for a payment problem, not that the subscriber cancelled — they did not.
- Leave card details to Google Play. Payment recovery happens in Play's own subscription management, not in your app.
The snippet runs on your server: the API key belongs there, not in an app binary.
On the App Store
Apple's vocabulary differs: there is a billing grace period and a billing retry, but no state named "account hold". Tierux maps a DID_FAIL_TO_RENEW notification with the grace-period subtype to grace_period with access on, using Apple's grace-period expiry date as expiresAt; without that subtype, and on GRACE_PERIOD_EXPIRED, it writes account_hold with access off and a billing_retry event. The App Store notification path is implemented with limitations — see the platform support matrix.
Glossary: Grace period
The entitled part of payment recovery, on Play and on the App Store.
Glossary: Account hold
Play's suspended phase after grace, and how recovery from it works.
Glossary: Billing retry
Why retry is a process that spans both phases rather than one state.
Docs: Webhooks
The outbound event contract, including grace_period and billing_retry.
Guide: The entitlement state machine
When to refresh and when to trust the cached state.
Handle Play subscription states before they cost you a subscriber
Free tier — unlimited apps, 1 paywall. No credit card, no revenue share.
Start free