Guide · Chapter 2 of 4

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:

  1. Grace period. Play sends SUBSCRIPTION_IN_GRACE_PERIOD. Tierux writes status grace_period with access on, and appends a grace_period event to the timeline.
  2. 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 status account_hold with access off, and appends a billing_retry event.
  3. 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 — normally active with a new expiresAt. If the window closes unrecovered, SUBSCRIPTION_EXPIRED writes expired with 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;

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.

Related reading

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.

← Back to the subscription lifecycle guide

Handle Play subscription states before they cost you a subscriber

Free tier — unlimited apps, 1 paywall. No credit card, no revenue share.

Start free