Skip to content

Subscription lifecycle

The interactive simulator on the homepage walks through these transitions. This page is the reference behind it.

Your code should gate on whether the entitlement is active, and separately show UI based on whether it will renew. Confusing the two is the most common lifecycle bug: a customer who turns off auto-renew is still entitled until the period ends.

Event App Store signal Google Play (RTDN) Stripe Entitlement after Your app
Purchase SUBSCRIBED SUBSCRIPTION_PURCHASED invoice.paid Active Refresh entitlements; server records it
Renewal DID_RENEW SUBSCRIPTION_RENEWED invoice.paid Active, new expiry Nothing
Auto-renew off DID_CHANGE_RENEWAL_STATUS SUBSCRIPTION_CANCELED customer.subscription.updated Active until period end Keep access; optional win-back
Payment failed DID_FAIL_TO_RENEW SUBSCRIPTION_IN_GRACE_PERIOD invoice.payment_failed Active during grace Keep access; prompt to fix payment
Recovered DID_RENEW (billing recovery) SUBSCRIPTION_RECOVERED invoice.paid Active Nothing
Period ends EXPIRED SUBSCRIPTION_EXPIRED customer.subscription.deleted Inactive Remove access on next check
Refund / revoke REFUND, REVOKE SUBSCRIPTION_REVOKED charge.refunded Inactive immediately Remove access now
Restore (new device) StoreKit current entitlements queryPurchases , Unchanged identify(email) then read entitlements

The store signal names are the platforms’ own. Orca’s webhook does not forward them; it sends the updated entitlement, and the server SDKs’ getActiveEntitlements reflects the current state.

Grace-period length is configured in App Store Connect, Play Console or Stripe’s retry settings, not in Orca. Apple’s billing retry and Google’s account hold can continue after grace ends; during those windows the entitlement is no longer active even though the store may still recover the payment later.

App Store and Google Play prorate automatically once an Offering is configured in the dashboard. For Stripe and GoCardless, pass what the customer is switching from:

await orca.purchase(newEntitlement,
externalStore: ExternalStore.stripe,
redirectUrl: 'https://yourapp.com/success',
failureRedirectUrl: 'https://yourapp.com/failure',
proratedProduct: currentEntitlement.products.stripe,
prorationMode: ProrationMode.upgrade);

Access follows the customer email. A purchase made on iOS by ada@example.com is active on Android and web for ada@example.com once each client calls identify. A purchase made without identifying the customer cannot later be found by email, identify before selling.

The simulator collapses billing-retry and account-hold states into “grace” and “expired”, shows one store at a time, and omits one-time purchases and consumables.