Subscription lifecycle
How purchases, renewals, cancellations, refunds and revocations change a person's access and which webhooks fire.
A subscription moves from purchase through renewals to its end. Voidhash learns each change from the store or payment source, updates the person's entitlements, and sends a webhook when the change matters to your backend.
Gate access on perks, not on subscription status. A perk stays active in every state that still entitles the person, so one check covers trials, grace periods and pending cancellations.
How Voidhash learns about changes
| Provider | Source of changes |
|---|---|
| App Store | The SDK reports each transaction; App Store Server Notifications report later changes. |
| Google Play | The SDK reports each purchase; Real-time developer notifications (RTDN) report later changes. |
| Stripe | Stripe events such as invoice.paid, customer.subscription.updated, customer.subscription.deleted and charge.refunded, each read back from Stripe. |
| Payment source | Your provider endpoint reports transactions and subscription states. These events carry verified: false. |
Voidhash processes each store event once: a repeat sends no second webhook, and a late event can't move a subscription back to an older state. An event for an unmapped store product is applied once you map it, if you do so within a year. See Unmapped purchases.
States
A refund, revocation or immediate cancellation ends a subscription at once (dashed).
Statuses
Voidhash stores two statuses, which webhooks report in status. A subscription is canceled once
it expired, was revoked, or was refunded on the App Store. Until then it is active, including
trials, grace periods and auto-renew off, which sets cancelAtPeriodEnd and canceledAt instead.
The SDKs read a detailed status from what the store reports. Each subscription in the person's
subscriptions carries it as status, with periodType (trial, intro or regular) and
willRenew:
| Situation | API and webhook status | SDK status | People label |
|---|---|---|---|
| Free trial | active, isTrial: true | trialing | Active |
| Paid, including an offer's discounted period | active | active | Active |
| Auto-renew off, period not over | active, cancelAtPeriodEnd: true | active or trialing, willRenew: false | Cancels at period end |
| Renewal failed, in a grace period | active | in_grace_period | Grace period |
| Renewal failed, the store retries without access | active | in_billing_retry | Expired |
| Google Play account hold | active | on_hold | Expired |
| Paused on Google Play | active | paused | Expired |
| End date passed by over 24 hours, store hasn't reported the end | active | expired | Expired |
| Ended: expired or canceled at once | canceled | expired | Expired (Canceled while its end date is still ahead) |
| Access withdrawn: revoked, or refunded on the App Store | canceled | revoked | Expired |
subscriptions.all lists every subscription that is neither expired nor revoked, best first:
active, then trialing, in_grace_period, in_billing_retry, on_hold and paused, the latest
to expire first. subscriptions.current is the first of them, or null. A subscription whose perks
still apply, in a grace period or within the 24-hour buffer below, stays listed. subscriptions.history
lists every subscription, ended ones included, with the same status. Use these for account
screens, such as "Renews on" or "Update your payment method"; gate features on perks.
Each subscription also reports what changes next: renewsToProductSlug for a downgrade or crossgrade
the store applies at renewal, pendingPrice for a price change with whether the customer agreed to it
(consent), resumesAt for a pause, billingIssueDetectedAt and gracePeriodExpiresAt for a failed
renewal, and refundedAt and revokedAt.
When perks end
Perks stay active until the later of expiry and the end of any grace period, plus 24 hours for late renewal notifications. A revoked or ended subscription loses its perks at once. A one-time purchase keeps them until it is refunded or revoked; a partial refund doesn't remove them.
Grace periods and billing retry
When a renewal payment fails, the store retries it. If you turned on a grace period (Billing
Grace Period in App Store Connect, or a grace period in Google Play Console), the person keeps
access while the store retries: Voidhash keeps their perks until the grace period ends, plus the 24
hours above. The subscription stays active and no webhook fires. Without a grace period, perks end
at the expiry date plus 24 hours.
If the payment recovers, the subscription renews (subscription.renewed) and the perks return.
When a Google Play grace period ends unpaid, Google puts the subscription on account hold: its perks
end at once, and it reads as on_hold until a payment recovers it or Google ends it. Payment
sources report past_due, which starts billing retry without a grace period.
Lifecycle events
| Change | Access | Webhook |
|---|---|---|
| Purchase | Perks granted | subscription.created |
| Free trial starts | Perks granted; isTrial: true | subscription.created |
| Trial converts or the subscription renews | Perks extended to the new expiry | subscription.renewed |
| Auto-renew turned off | Perks kept until expiry | subscription.cancelled |
| Auto-renew turned back on | No change | None |
| Grace period or billing retry | Perks kept, see Grace periods | None |
| Google Play account hold or pause | Perks end until a payment resumes it | None |
| Payment recovered | Perks extended | subscription.renewed |
| Expiration | Perks end | subscription.expired with expirationReason: "ended" |
| Revocation (Family Sharing removed, Google revoke) | Perks end at once | subscription.expired with expirationReason: "revoked" |
| Refund | Depends on the store, see below | purchase.refunded |
| Product change | New product's perks | Depends on the store, see below |
| One-time or consumable purchase | Perks granted | purchase.completed |
| One-time purchase refunded or revoked | Perks end | purchase.refunded or purchase.revoked |
subscription.cancelled means auto-renew is off, not that access ended; end access on
subscription.expired. A subscription Stripe ends immediately sends only subscription.expired.
See Webhook events for payloads.
Store differences
Trials
- App Store: only a free-trial introductory offer counts as a trial. Pay-up-front and pay-as-you-go offers are paid periods.
- Google Play: only a free-trial phase is a trial; an introductory-price phase is a paid period
with
periodType: "intro". An offer whose phase Google doesn't report is not a trial either, and its price is recorded once Google reports the order rather than guessed from the base plan. - Stripe: a period that starts before
trial_endis a trial.
Refunds
- App Store: refunding the current period ends the subscription and its perks at the refund
date and sends
subscription.expired; the SDKs read it asrevoked. Refunding a past period changes nothing. - Google Play and Stripe: a refund records the money and sets the subscription's
refundedAt; access ends when the store expires or revokes the subscription. A lost Stripe dispute is a refund withrefundReason: "chargeback_lost". - Payment sources: a reported refund doesn't end the subscription.
A reversed App Store refund restores the period. No webhook fires for it.
Product changes
| Store | Upgrade | Downgrade |
|---|---|---|
| App Store | Immediate; same subscription; subscription.renewed | Waits for the next renewal; subscription.renewed then |
| Google Play | A new subscription replaces the old: subscription.created for it, subscription.expired for the old | Deferred: the new subscription keeps the old product, with renewsToProductSlug naming the new one, and switches at its renewal (subscription.renewed). The new product's perks start then |
| Stripe | subscription.product_changed at the switch, then subscription.renewed when it is charged | Applied when the scheduled change takes effect |
Other differences
- Resubscribing after expiry continues the same subscription on the App Store
(
subscription.renewed). Google Play and Stripe start a new one (subscription.created). - Pausing on Google Play ends the perks while the subscription is paused (
paused, withresumesAt); the payment that resumes it renews it. - Non-renewing subscriptions on the App Store mapped to a subscription product end one billing duration after purchase. Mapped to a non-consumable, they are a one-time purchase.
- Payment sources report
trialing,active,past_due,canceledorexpired.past_duestarts billing retry without a grace period.
Simulate the lifecycle
In the test environment, renew, expire, revoke, refund or start a 3-day grace period on a
simulated purchase with the MCP tool simulate_sandbox_event,
POST /api/v1/projects/{projectId}/sandbox/actions with a secret key, or the CLI's
voidhash sandbox renew, expire, grace-period, refund or revoke. Access changes at once,
and webhooks follow. See Simulate renewals, expiry and refunds.
Next steps
- Webhook events: every payload field.
- Check access from your backend with Node.js, PHP, Go, Rust or over HTTP.
- Test purchases (Help center): store sandboxes and simulated purchases.
- Products and perks: what each purchase grants.
Something wrong or missing on this page? Tell us by email or on Discord.