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.

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.

InformationiOS ClientAndroid ClientServer (Both)
Auto-renew statuswillAutoRenewisAutoRenewing
Next renewal productautoRenewPreference
Pending upgrade/downgradependingUpgradeProductIdpendingPurchaseUpdateAndroid
Expiration reasonexpirationReason
Grace period statusgracePeriodExpirationDate
Billing retry statusisInBillingRetry⚠️ isSuspendedAndroid (state only, no retry details)
Renewal daterenewalDate
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 onlyIncludeActiveItemsIOS to true, so iOS reads Transaction.currentEntitlements; pass { onlyIncludeActiveItemsIOS: false } to read Transaction.all including expired and revoked entries. Android's queryPurchasesAsync returns 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.

1. initConnection()
2. getAvailablePurchases() → [PurchaseIOS]
3. For each purchase:
→ check purchaseState
→ validate with server
→ update local entitlements
→ finishTransaction() for unfinished transactions
Note: iOS transaction queue persists unfinished transactions

New Purchase Flow#

When a user initiates a new subscription purchase. Purchase states differ between platforms:

1. requestPurchase({ request: { apple: { sku } }, type: 'subs' }) → StoreKit payment sheet
2. purchaseUpdatedListener receives PurchaseIOS
→ validate → deliver → finishTransaction()
A delivered 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:

1. getActiveSubscriptions() → [ActiveSubscription]
2. For each subscription:
• isActive = true → grant access
• check renewalInfoIOS for details:
- willAutoRenew = false → show renewal prompt
- isInBillingRetry = true → show payment issue
- pendingUpgradeProductId → show pending change
• check expirationDateIOS on the subscription → show expiry info
3. No active subscriptions → revoke access

Detecting Cancellations#

Users can cancel subscriptions at any time. The subscription remains active until expiration.

Detection: willAutoRenew = false
User still has access:
• Until expirationDateIOS on ActiveSubscription
Actions:
→ Show "subscription ends on [expirationDateIOS]"
→ Offer re-subscribe option
→ Keep access until expiration

Handling Expiration#

When a subscription expires (cancelled + period ended), revoke access:

1. getActiveSubscriptions() returns empty or no matching product
2. Verify with server (recommended)
3. Revoke premium access
4. Show re-subscribe prompt

Restoring Purchases#

Users may need to restore subscriptions on new devices or after reinstalling:

1. User taps "Restore Purchases"
2. getAvailablePurchases() → [PurchaseIOS]
3. StoreKit fetches from Apple ID's purchase history
4. Validate each purchase with server
5. Grant access for valid subscriptions
6. finishTransaction() for each restored purchase
Note: iOS requires "Restore Purchases" button per App Store guidelines

Example Scenario#

Understanding how subscription states change over time helps implement correct handling:

Timeline: Cancelled Subscription
Day 1: User cancels subscription
• Subscription still valid until Day 30 (billing period end)
• iOS: willAutoRenew = false
• Android: isAutoRenewing = false
Day 15: User restores purchases
• getAvailablePurchases() returns the purchase (not expired)
• ✅ Grant access (still valid until Day 30)
Day 35: User restores purchases
• iOS: currentEntitlements returns empty (expired)
• Android: queryPurchases returns empty (expired)
• ✅ No access (correctly expired)

This is why server validation is critical:

1. User purchases subscription
2. User requests refund from Apple/Google
3. Refund is approved
Without server validation:
• getAvailablePurchases() may still return the purchase temporarily
• ❌ App grants access (incorrect - refunded!)
With server validation:
• Server detects refund via RTDN/App Store Notifications
• ✅ Server denies access (correct)

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 productId and autoRenewPreference.
  • 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. renewalOfferType carries 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:

  1. Immediately after upgrade: The productId may still show the old tier (monthly), but autoRenewPreference shows the new tier (yearly). The pendingUpgradeProductId is set to the new tier.
  2. After processing (few minutes): The productId updates to the new tier (yearly), and pendingUpgradeProductId becomes null since 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 productId is 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: isActive is true but willAutoRenew is false. User has access until expiration.
  • Grace Period: gracePeriodExpirationDate has a value. Billing failed but user still has access temporarily.
  • Billing Retry: isInBillingRetry is 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.

Summary#

iOS

  • Rich client-side data via RenewalInfoIOS
  • Use pendingUpgradeProductId for 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, and pendingPurchaseUpdateAndroid
  • Server-side required for detailed subscription info
  • Use Google Play Developer API for authoritative data
  • RTDN for real-time subscription updates