Subscription
Understanding how subscriptions work on each platform is crucial for proper implementation. iOS and Android handle subscription data very differently, especially when it comes to renewal information.
Live example (full subscription flow — fetch, purchase, upgrade / downgrade, cancellation, restore): expo-iap · react-native-iap · flutter_inapp_purchase · kmp-iap
Platform Comparison#
The key difference is where subscription information is available. iOS provides rich data client-side, while Android requires server-side calls for detailed information.
| Information | iOS Client | Android Client | Server (Both) |
|---|---|---|---|
| Auto-renew status | ✅ willAutoRenew | ✅ isAutoRenewing | ✅ |
| Next renewal product | ✅ autoRenewPreference | ❌ | ✅ |
| Pending upgrade/downgrade | ✅ pendingUpgradeProductId | ✅ pendingPurchaseUpdateAndroid | ✅ |
| Expiration reason | ✅ expirationReason | ❌ | ✅ |
| Grace period status | ✅ gracePeriodExpirationDate | ❌ | ✅ |
| Billing retry status | ✅ isInBillingRetry | ⚠️ isSuspendedAndroid (state only, no retry details) | ✅ |
| Renewal date | ✅ renewalDate | ❌ | ✅ |
| Detailed subscription state | ✅ | ❌ | ✅ |
iOS: Rich client-side data via RenewalInfoIOS, but server validation is still recommended for production apps.
Android: client-side subscription lifecycle data is limited to isAutoRenewing, isSuspendedAndroid, and pendingPurchaseUpdateAndroid — the purchase itself still carries productId, purchaseToken, transactionDate, and purchaseState. Expiry and renewal dates and detailed subscription state require server-side validation.
Both platforms: Use getActiveSubscriptions or getAvailablePurchases to verify purchases client-side, and implement server-side validation for authoritative subscription status.
Recommendation: Use IAPKit for server-side verification to get unified subscription data across both platforms—including all the iOS-only fields for Android subscriptions.
Purchase Verification#
Regardless of platform, you should verify purchases using OpenIAP's APIs. These APIs retrieve the latest subscription data from the store and provide a unified interface.
Client-Side Verification#
Use these APIs to check subscription status in your app:
- getActiveSubscriptions: Returns only currently active subscriptions. Best for checking entitlements.
- getAvailablePurchases: Returns the purchases the store still holds — owned non-consumables, active subscriptions, and unfinished transactions. It is not a purchase-history source on either platform. The framework SDKs default
onlyIncludeActiveItemsIOStotrue, so iOS readsTransaction.currentEntitlements; pass{ onlyIncludeActiveItemsIOS: false }to readTransaction.allincluding expired and revoked entries. Android'squeryPurchasesAsyncreturns only currently-owned purchases and has no equivalent option. For full iOS history use getAllTransactionsIOS.
These APIs query the store directly and return the latest data, including renewal information on iOS.
Server-Side Verification (Recommended)#
For production apps, implement server-side validation for authoritative subscription status:
- iOS: App Store Server API + App Store Server Notifications V2
- Android: Google Play Developer API + RTDN (Real-time Developer Notifications)
Setting up server-side verification can be complex. OpenIAP's hosted backend IAPKit provides a simple, unified API for server-side receipt validation across both iOS and Android platforms.
With IAPKit, you can verify purchases, manage subscriptions, and handle webhooks without building complex server infrastructure from scratch.
Learn more about IAPKit integration in our announcement.
- Authoritative source: Client data can be manipulated; server data is trusted
- Cross-platform sync: If users access your service from web or other platforms
- Background updates: Subscriptions can renew, cancel, or expire when app isn't running
- Fraud prevention: Detect and prevent receipt manipulation
- Analytics: Track subscription metrics and revenue server-side
Subscription Lifecycle#
This section shows how to handle subscription states throughout the app lifecycle. The flows apply to both iOS and Android unless noted.
On App Launch#
Check for existing subscriptions when the app starts. This handles purchases made while the app was closed.
New Purchase Flow#
When a user initiates a new subscription purchase. Purchase states differ between platforms:
PurchaseIOS always carries purchaseState: 'purchased' — StoreKit only hands the listener completed transactions, so the pending and unknown members of the shared enum never appear on iOS. Anything that is not a completed purchase arrives through purchaseErrorListener instead: Ask to Buy and other deferred payments as ErrorCode.DeferredPayment ('deferred-payment'), cancellations as ErrorCode.UserCancelled.Checking Subscription Status#
Periodically verify subscription status, especially for subscription state changes:
Detecting Cancellations#
Users can cancel subscriptions at any time. The subscription remains active until expiration.
Handling Expiration#
When a subscription expires (cancelled + period ended), revoke access:
Restoring Purchases#
Users may need to restore subscriptions on new devices or after reinstalling:
Example Scenario#
Understanding how subscription states change over time helps implement correct handling:
This is why server validation is critical:
When to Validate#
Server validation is needed at these key points:
- After purchase — Verify the purchase is legitimate
- On restore — Check current status (active/cancelled/refunded/expired)
- Periodically for active subscriptions — Detect refunds and cancellations
- On app launch — Sync subscription state with server
iOS: You can get purchase data from Transaction.all (including expired ones), but there may be synchronization delays between devices. The client can check expiry/renewal info, but server validation is still recommended for security.
Android: Server-side validation is mandatory for subscription management. The client can't access expiry time — you must use Google Play Developer API to get subscription status, renewal dates, grace periods, etc. This is why server-side purchase history management is essential.
- Always finish transactions: Unfinished transactions will keep appearing on app launch. Call
finishTransaction()after validation and content delivery. - Android 3-day window: Android purchases must be acknowledged within 3 days or they're automatically refunded.
- Server validation: Client-side checks can be bypassed. Always validate with your server for authoritative subscription status.
- Background renewals: Subscriptions renew when app isn't running. Use server notifications (RTDN for Android, App Store Server Notifications for iOS) to track real-time changes.
iOS Subscription Overview#
iOS provides rich subscription data client-side through StoreKit 2. The RenewalInfoIOS type contains detailed renewal information that lets you build subscription management UI without server calls. However, server validation is still recommended for production apps.
RenewalInfoIOS Fields#
This type is available on PurchaseIOS and ActiveSubscription via the renewalInfoIOS property:
- willAutoRenew: Whether the subscription will automatically renew. If
false, the user has cancelled but still has access until expiry. - autoRenewPreference: The product ID that will be used at the next renewal. If different from the current product, the user has scheduled a tier change.
- pendingUpgradeProductId: Convenience field showing the pending tier change target. Calculated by comparing
productIdandautoRenewPreference. - renewalDate: Next renewal date (timestamp in milliseconds).
- expirationReason: StoreKit's raw integer expiration-reason value represented as a string (
"1","2", …), not a symbolic name. Preserve unknown future values rather than mapping them to a fallback. - gracePeriodExpirationDate: Grace period end date if in grace period due to billing issues.
- isInBillingRetry: Whether Apple is currently retrying a failed payment.
- renewalOfferId / renewalOfferType: The offer applied to the next renewal.
renewalOfferTypecarries values such as"PROMOTIONAL","SUBSCRIPTION_OFFER_CODE", and"WIN_BACK".
Detecting Tier Changes (Upgrade/Downgrade)#
Understanding how tier changes work on iOS is crucial for proper subscription management. The behavior differs between upgrades and downgrades.
Upgrade Flow#
When a user upgrades (e.g., monthly → yearly), Apple processes the change immediately with a prorated refund for the remaining time on the old plan. However, the purchase data updates in stages:
- Immediately after upgrade: The
productIdmay still show the old tier (monthly), butautoRenewPreferenceshows the new tier (yearly). ThependingUpgradeProductIdis set to the new tier. - After processing (few minutes): The
productIdupdates to the new tier (yearly), andpendingUpgradeProductIdbecomesnullsince there's no longer a pending change.
Check pendingUpgradeProductId. If it has a value different from productId, there's a pending tier change:
- pendingUpgradeProductId exists: Show UI indicating "Your subscription will change to [new tier]"
- pendingUpgradeProductId is null: No pending change; the current
productIdis the active subscription
This logic is already calculated in pendingUpgradeProductId by comparing productId with autoRenewPreference.
Downgrade Flow#
Downgrades (e.g., yearly → monthly) are scheduled to take effect at the end of the current billing period:
- productId: Shows current tier (yearly) - user keeps premium access
- autoRenewPreference: Shows future tier (monthly)
- pendingUpgradeProductId: Shows monthly (pending downgrade)
The user retains their current tier until expiry, then switches to the lower tier.
Other Subscription States#
- Cancellation:
isActiveis true butwillAutoRenewis false. User has access until expiration. - Grace Period:
gracePeriodExpirationDatehas a value. Billing failed but user still has access temporarily. - Billing Retry:
isInBillingRetryis true. Apple is retrying the payment.
Server-Side Validation#
While iOS provides rich client-side data, server validation is still recommended:
- App Store Server API: Verify subscription status and get transaction history
- App Store Server Notifications V2: Receive real-time webhook events (renewals, cancellations, refunds, Family Sharing changes)
Server validation is especially important for cross-platform apps, fraud prevention, and accurate analytics.
Related APIs#
- getActiveSubscriptions - Get active subscriptions with renewal info
- getAvailablePurchases - Get all purchases including expired
- subscriptionStatusIOS - Get detailed subscription status
- RenewalInfoIOS - Type reference
Summary#
iOS
- Rich client-side data via
RenewalInfoIOS - Use
pendingUpgradeProductIdfor tier change detection - Server-side recommended for production apps
- App Store Server Notifications V2 for webhooks
Android
- Client-side subscription lifecycle data limited to
isAutoRenewing,isSuspendedAndroid, andpendingPurchaseUpdateAndroid - Server-side required for detailed subscription info
- Use Google Play Developer API for authoritative data
- RTDN for real-time subscription updates
- Life Cycle - Overall purchase lifecycle
- Subscription Upgrade/Downgrade - Tier change implementation
- Types: RenewalInfoIOS - Type reference
- APIs: getActiveSubscriptions - API reference