Skip to main content
Version: 1.3 (Current)

Subscription Flow

IAPKit - In-App Purchase Made Simple

Key Concepts

  • Use ProductQueryType.Subs to fetch subscription products
  • Android requires offerToken for subscription purchases
  • Use getActiveSubscriptions() to check subscription status
  • Always set isConsumable = false when finishing subscription transactions

Fetch Subscriptions

val subscriptions = kmpIapInstance.fetchProducts {
skus = listOf("premium_monthly", "premium_yearly")
type = ProductQueryType.Subs
}

Check Active Subscriptions

// Quick check
val hasActive = kmpIapInstance.hasActiveSubscriptions(subscriptionIds)

// Detailed info
val activeSubscriptions = kmpIapInstance.getActiveSubscriptions(subscriptionIds)
activeSubscriptions.forEach { sub ->
println("Product: ${sub.productId}")
println("Expires: ${sub.expirationDateIOS}") // iOS
println("Auto-renewing: ${sub.autoRenewingAndroid}") // Android
}

Request Subscription

// Get offer token for Android
val product = subscriptions.find { it.productId == "premium_monthly" }
val offerToken = product?.subscriptionOffers?.firstOrNull()?.offerToken

kmpIapInstance.requestPurchase {
apple { sku = "premium_monthly" }
google { // Recommended (v1.3.15+)
skus = listOf("premium_monthly")
subscriptionOffers = offerToken?.let {
listOf(SubscriptionOfferAndroid(sku = "premium_monthly", offerToken = it))
}
}
}

Finish Transaction

kmpIapInstance.finishTransaction(
purchase = purchase.toPurchaseInput(),
isConsumable = false // Subscriptions are never consumable
)

Platform-Specific Info

FeatureiOSAndroid
ExpirationexpirationDateIOSServer-side
EnvironmentenvironmentIOSN/A
Auto-renewN/AautoRenewingAndroid
Manage subsApp StoredeepLinkToSubscriptions()

Android Subscription Management

if (kmpIapInstance.getPlatform() == IapPlatform.Android) {
kmpIapInstance.deepLinkToSubscriptions("premium_monthly")
}

IAPKit Server Verification

For subscription apps, server-side verification is critical for managing access control and handling renewals.

Setup

  1. Get your API key from iapkit.com
  2. Configure environment variables (see Purchase Flow for details)

Subscription Verification

kmpIapInstance.purchaseUpdatedListener.collect { purchase ->
val purchaseToken = purchase.purchaseToken
if (purchaseToken.isNullOrEmpty()) {
showError("No purchase token available")
return@collect
}

try {
val result = kmpIapInstance.verifyPurchaseWithProvider(
VerifyPurchaseWithProviderProps(
provider = PurchaseVerificationProvider.Iapkit,
iapkit = RequestVerifyPurchaseWithIapkitProps(
apiKey = AppConfig.iapkitApiKey,
apple = RequestVerifyPurchaseWithIapkitAppleProps(jws = purchaseToken),
google = RequestVerifyPurchaseWithIapkitGoogleProps(purchaseToken = purchaseToken)
)
)
)

when (result.iapkit?.state) {
IapkitPurchaseState.Entitled -> {
// Active subscription - grant access
grantSubscriptionAccess(purchase.productId)
kmpIapInstance.finishTransaction(
purchase = purchase.toPurchaseInput(),
isConsumable = false
)
}
IapkitPurchaseState.Expired -> {
// Subscription ended - revoke access
revokeSubscriptionAccess(purchase.productId)
}
IapkitPurchaseState.Canceled -> {
// User canceled but may still have access until period ends
showInfo("Subscription will end at period end")
}
IapkitPurchaseState.Inauthentic -> {
// Fraudulent purchase detected
revokeSubscriptionAccess(purchase.productId)
showError("Invalid purchase detected")
}
else -> {
showError("Unknown state: ${result.iapkit?.state}")
}
}
} catch (e: Exception) {
showError("Verification error: ${e.message}")
}
}

Verification Response

{
"isValid": true,
"state": "ENTITLED",
"store": "APPLE"
}
// Access in code
val isValid = result.iapkit?.isValid // true
val state = result.iapkit?.state // IapkitPurchaseState.Entitled
val store = result.iapkit?.store // IapStore.Apple or IapStore.Google

Periodic Validation

For production apps, validate subscriptions periodically:

// On app launch or every 24 hours
suspend fun validateActiveSubscriptions() {
val activeSubscriptions = kmpIapInstance.getActiveSubscriptions(subscriptionIds)

activeSubscriptions.forEach { subscription ->
val result = verifyWithIapkit(subscription)

when (result.iapkit?.state) {
IapkitPurchaseState.Entitled -> { /* Still valid */ }
IapkitPurchaseState.Expired,
IapkitPurchaseState.Canceled -> {
revokeSubscriptionAccess(subscription.productId)
}
else -> { /* Handle error */ }
}
}
}

IAPKit Subscription States

StateDescriptionAction
entitledUser has an active, valid subscriptionGrant access
expiredSubscription has expired and not renewedRemove access
canceledUser cancelled but may still have access until period endsCheck expiration date
pendingPayment is pending (e.g., awaiting parental approval)Show pending UI
pending-acknowledgmentPurchase needs acknowledgment (Android)Call finishTransaction()
inauthenticPurchase could not be verified / fraudulentDeny access

Checking Subscription Status on App Launch

Android Renewal Detection

On Android, purchaseUpdatedListener does not fire for renewals that occurred while the app was closed. Always verify subscription status on app launch.

class SubscriptionViewModel : ViewModel() {
private val subscriptionIds = listOf("premium_monthly", "premium_yearly")

init {
viewModelScope.launch {
kmpIapInstance.initConnection()
checkSubscriptionStatusOnLaunch()
}
}

private suspend fun checkSubscriptionStatusOnLaunch() {
try {
val purchases = kmpIapInstance.getAvailablePurchases()

val subscriptionPurchase = purchases.find {
subscriptionIds.contains(it.productId)
}

if (subscriptionPurchase == null) {
// No subscription found
updateUIForExpiredSubscription()
return
}

// Verify with IAPKit for authoritative status
val result = kmpIapInstance.verifyPurchaseWithProvider(
VerifyPurchaseWithProviderProps(
provider = PurchaseVerificationProvider.Iapkit,
iapkit = RequestVerifyPurchaseWithIapkitProps(
apiKey = AppConfig.iapkitApiKey,
apple = RequestVerifyPurchaseWithIapkitAppleProps(
jws = subscriptionPurchase.purchaseToken
),
google = RequestVerifyPurchaseWithIapkitGoogleProps(
purchaseToken = subscriptionPurchase.purchaseToken
)
)
)
)

when (result.iapkit?.state) {
IapkitPurchaseState.Entitled -> {
grantSubscriptionAccess(subscriptionPurchase.productId)
}
IapkitPurchaseState.Expired -> {
revokeSubscriptionAccess(subscriptionPurchase.productId)
showRenewalPrompt()
}
IapkitPurchaseState.Canceled -> {
// Still has access until period ends
showCancellationNotice()
}
else -> {
handleUnknownState(result.iapkit?.state)
}
}
} catch (e: Exception) {
// Grant temporary access on error to avoid penalizing users
grantTemporaryAccess()
}
}
}

Complete Verification Flow

Here's a complete example of handling subscription verification within purchaseUpdatedListener:

private suspend fun onPurchaseSuccess(purchase: Purchase) {
val purchaseToken = purchase.purchaseToken
if (purchaseToken.isNullOrEmpty()) {
showError("No purchase token available")
return
}

try {
val result = kmpIapInstance.verifyPurchaseWithProvider(
VerifyPurchaseWithProviderProps(
provider = PurchaseVerificationProvider.Iapkit,
iapkit = RequestVerifyPurchaseWithIapkitProps(
apiKey = AppConfig.iapkitApiKey,
apple = RequestVerifyPurchaseWithIapkitAppleProps(jws = purchaseToken),
google = RequestVerifyPurchaseWithIapkitGoogleProps(purchaseToken = purchaseToken)
)
)
)

when (result.iapkit?.state) {
IapkitPurchaseState.Entitled -> {
// 1. Grant access
grantSubscriptionAccess(purchase.productId)

// 2. Finish the transaction
kmpIapInstance.finishTransaction(
purchase = purchase.toPurchaseInput(),
isConsumable = false
)

// 3. Show success message
showSuccess("Subscription activated!")
}

IapkitPurchaseState.PendingAcknowledgment -> {
// Android: Transaction needs acknowledgment
kmpIapInstance.finishTransaction(
purchase = purchase.toPurchaseInput(),
isConsumable = false
)
grantSubscriptionAccess(purchase.productId)
}

IapkitPurchaseState.Pending -> {
// Purchase still processing (e.g., pending payment)
showPendingUI(purchase.productId)
}

IapkitPurchaseState.Expired,
IapkitPurchaseState.Canceled -> {
revokeSubscriptionAccess(purchase.productId)
}

IapkitPurchaseState.Inauthentic -> {
revokeSubscriptionAccess(purchase.productId)
logSecurityEvent("Inauthentic purchase detected", purchase)
}

else -> {
showError("Unexpected state: ${result.iapkit?.state}")
}
}
} catch (e: Exception) {
// Handle verification errors gracefully
// Consider granting temporary access rather than blocking the user
showError("Verification error: ${e.message}")
}
}

For more details on subscription validation patterns, see Subscription Validation.

Android basePlanId Limitation

Client-Side Limitation

The basePlanId is available when fetching products, but not when retrieving purchases via getAvailablePurchases(). This is a limitation of Google Play Billing Library - the purchase token alone doesn't reveal which base plan was purchased.

See GitHub Issue #3096 for more details.

Why this matters:

  • If you have multiple base plans (e.g., monthly, yearly, premium), you cannot determine which plan the user is subscribed to using client-side APIs alone
  • The basePlanId is only available from subscriptionOfferDetailsAndroid at the time of purchase, not from restored purchases

Solution: Server-Side Verification with IAPKit

Use the verifyPurchaseWithProvider function to get complete subscription details including basePlanId:

val verifyAndroidSubscription = suspend fun(purchase: Purchase) {
val result = kmpIapInstance.verifyPurchaseWithProvider(
VerifyPurchaseWithProviderProps(
provider = PurchaseVerificationProvider.Iapkit,
iapkit = RequestVerifyPurchaseWithIapkitProps(
apiKey = AppConfig.iapkitApiKey,
google = RequestVerifyPurchaseWithIapkitGoogleProps(
purchaseToken = purchase.purchaseToken
)
)
)
)

// Response includes offerDetails.basePlanId in lineItems
val basePlanId = result.providerResponse
?.get("lineItems")
?.let { (it as? List<*>)?.firstOrNull() as? Map<*, *> }
?.get("offerDetails")
?.let { (it as? Map<*, *>)?.get("basePlanId") as? String }

println("Subscribed to base plan: $basePlanId")
}

See verifyPurchaseWithProvider

The server response includes offerDetails.basePlanId in the lineItems array, allowing you to identify exactly which subscription plan the user purchased.

Subscription Offers

When fetching products, each subscription offer includes: basePlanId, offerId?, offerTags, offerToken, and pricingPhases. See ProductSubscriptionAndroidOfferDetails for more details.

See IAPKit documentation for setup instructions and API details.

Next Steps