Alternative Billing
This guide explains how to implement alternative billing functionality in your app using kmp-iap, allowing you to use external payment systems alongside or instead of the App Store/Google Play billing.
Official Documentation
Apple (iOS)
- StoreKit External Purchase Documentation - Official StoreKit external purchase API reference
- External Purchase Link Entitlement - Entitlement configuration
- ExternalPurchaseCustomLink API - Custom link API documentation
- OpenIAP External Purchase - OpenIAP external purchase specification
Google Play (Android)
- Alternative Billing APIs - Official Android alternative billing API guide
- User Choice Billing Overview - Understanding user choice billing
- User Choice Billing Pilot - Enrollment and setup
- Payments Policy - Google Play's payment policy
- UX Guidelines (User Choice) - User choice billing UX guidelines
- UX Guidelines (Alternative Billing) - Alternative billing UX guidelines
- EEA Alternative Billing - European Economic Area specific guidance
Platform Updates (2024)
iOS
- US apps can use StoreKit External Purchase Link Entitlement
- System disclosure sheet shown each time external link is accessed
- Commission: 27% (reduced from 30%) for first year, 12% for subsequent years
- EU apps have additional flexibility for external purchases
Android
- As of March 13, 2024: Alternative billing APIs must be used (manual reporting deprecated)
- Service fee reduced by 4% when using alternative billing (e.g., 15% → 11%)
- Available in South Korea, India, and EEA
- Gaming and non-gaming apps eligible (varies by region)
Overview
Alternative billing enables developers to offer payment options outside of the platform's standard billing systems:
- iOS: Redirect users to external websites for payment (iOS 16.0+)
- Android: Use Google Play's alternative billing options (requires approval)
Both platforms require special approval to use alternative billing:
- iOS: Must be approved for external purchase entitlement
- Android: Must be approved for alternative billing in Google Play Console
iOS Alternative Billing (External Purchase)
On iOS, alternative billing works by redirecting users to an external website where they complete the purchase.
Configuration
Configure iOS external purchase entitlements in your iOS project:
Entitlements (iosApp.entitlements):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Required: External purchase entitlement -->
<key>com.apple.developer.storekit.external-purchase</key>
<true/>
<!-- Optional: External purchase link entitlement -->
<key>com.apple.developer.storekit.external-purchase-link</key>
<true/>
</dict>
</plist>
Info.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Countries where external purchases are supported (ISO 3166-1 alpha-2 uppercase) -->
<key>SKExternalPurchase</key>
<dict>
<key>AllowedCountries</key>
<array>
<string>KR</string>
<string>NL</string>
<string>DE</string>
<string>FR</string>
</array>
</dict>
</dict>
</plist>
- Approval Required: You must obtain approval from Apple to use external purchase features
- URL Format: URLs must use HTTPS and be valid
- Supported Regions: Different features support different regions (EU, US, etc.)
See External Purchase Link Entitlement for details.
Basic Usage
import io.github.hyochan.kmpiap.kmpIapInstance
import io.github.hyochan.kmpiap.presentExternalPurchaseLinkIOS
// Present external purchase link
val result = kmpIapInstance.presentExternalPurchaseLinkIOS(
url = "https://your-site.com/checkout"
)
if (result.success) {
println("User was redirected to external URL")
} else {
println("Error: ${result.error}")
}
Important Notes
- iOS 16.0+ Required: External purchase links only work on iOS 16.0 and later
- No Purchase Callback: The
purchaseUpdatedListenerwill NOT fire when using external URLs - Deep Link Required: Implement deep linking to return users to your app after purchase
- Manual Validation: You must validate purchases on your backend server
Complete iOS Example
import io.github.hyochan.kmpiap.kmpIapInstance
import io.github.hyochan.kmpiap.presentExternalPurchaseLinkIOS
import kotlinx.coroutines.launch
fun handleExternalPurchase(productId: String) {
scope.launch {
try {
val result = kmpIapInstance.presentExternalPurchaseLinkIOS(
url = "https://your-site.com/checkout?product=$productId"
)
if (result.success) {
// User was redirected to external site
// Implement deep linking to handle return to app
println("Redirected to external checkout")
} else {
println("Error: ${result.error}")
}
} catch (e: Exception) {
println("Failed to present external link: ${e.message}")
}
}
}
Android Alternative Billing
Android supports two alternative billing modes:
- Alternative Billing Only: Users can ONLY use your payment system
- User Choice Billing: Users choose between Google Play or your payment system
Configuration
Set the billing mode when initializing the connection:
AlternativeBillingModeAndroid is deprecated. Use BillingProgramAndroid with enableBillingProgramAndroid instead.
// Before (deprecated)
val config = InitConnectionConfig(
alternativeBillingModeAndroid = AlternativeBillingModeAndroid.UserChoice
)
// After (recommended)
val config = InitConnectionConfig(
enableBillingProgramAndroid = BillingProgramAndroid.UserChoiceBilling
)
import io.github.hyochan.kmpiap.kmpIapInstance
import io.github.hyochan.kmpiap.openiap.BillingProgramAndroid
import io.github.hyochan.kmpiap.openiap.InitConnectionConfig
// Initialize with User Choice Billing (v1.3.0+)
val config = InitConnectionConfig(
enableBillingProgramAndroid = BillingProgramAndroid.UserChoiceBilling
// Or: BillingProgramAndroid.ExternalOffer
// Or: BillingProgramAndroid.ExternalPayments (Japan only, 8.3.0+)
)
val connected = kmpIapInstance.initConnection(config)
Mode 1: Alternative Billing Only
This mode requires a manual 3-step flow:
import io.github.hyochan.kmpiap.kmpIapInstance
suspend fun handleAlternativeBillingOnly(productId: String) {
try {
// Step 1: Check availability
val isAvailable = kmpIapInstance.checkAlternativeBillingAvailabilityAndroid()
if (!isAvailable) {
println("Alternative billing not available")
return
}
// Step 2: Show information dialog
val userAccepted = kmpIapInstance.showAlternativeBillingDialogAndroid()
if (!userAccepted) {
println("User declined alternative billing")
return
}
// Step 2.5: Process payment with your payment system
// ... your payment processing logic here ...
// Step 3: Create reporting token (after successful payment)
val token = kmpIapInstance.createAlternativeBillingTokenAndroid()
if (token != null) {
// Step 4: Report token to Google Play backend within 24 hours
reportToGoogleBackend(token)
println("Alternative billing completed")
} else {
println("Failed to create token")
}
} catch (e: Exception) {
println("Alternative billing error: ${e.message}")
}
}
Mode 2: User Choice Billing
With user choice, Google automatically shows a selection dialog:
import io.github.hyochan.kmpiap.kmpIapInstance
import io.github.hyochan.kmpiap.requestPurchase
import io.github.hyochan.kmpiap.openiap.BillingProgramAndroid
import io.github.hyochan.kmpiap.openiap.InitConnectionConfig
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
// Initialize with user choice mode (v1.3.0+)
val config = InitConnectionConfig(
enableBillingProgramAndroid = BillingProgramAndroid.UserChoiceBilling
)
kmpIapInstance.initConnection(config)
// Listen for user choice events
scope.launch {
kmpIapInstance.userChoiceBillingListener.collect { details ->
println("User selected alternative billing")
println("Products: ${details.products}")
println("Token: ${details.externalTransactionToken}")
// Process payment with your system
// ... your payment processing logic ...
// Report token to Google (token is provided in details)
reportToGoogleBackend(details.externalTransactionToken)
}
}
suspend fun handleUserChoicePurchase(productId: String) {
try {
// Request purchase - Google will show selection dialog
kmpIapInstance.requestPurchase {
android {
skus = listOf(productId)
}
}
// If user selects Google Play: purchaseUpdatedListener fires
// If user selects alternative: userChoiceBillingListener fires
println("Purchase requested")
} catch (e: Exception) {
println("Purchase error: ${e.message}")
}
}
Listening for User Choice Events
import io.github.hyochan.kmpiap.kmpIapInstance
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
// Listen for user choice billing events
scope.launch {
kmpIapInstance.userChoiceBillingListener.collect { details ->
println("User chose alternative billing")
println("Products: ${details.products}")
println("Token: ${details.externalTransactionToken}")
// Process payment with your system
processAlternativePayment(details.products)
// Report token to Google (token is already provided in details)
// No need to call createAlternativeBillingTokenAndroid() for UserChoice mode
reportToGoogleBackend(details.externalTransactionToken)
}
}
Mode 3: External Payments (8.3.0+, Japan Only)
External Payments is a new billing program introduced in Google Play Billing Library 8.3.0 that presents a side-by-side choice between Google Play Billing and the developer's external payment option directly in the purchase flow.
The External Payments program is currently only available for users in Japan.
Official Documentation
- External Payment Links - Official Google Play documentation
Configuration
Enable the External Payments program in the InitConnectionConfig:
import io.github.hyochan.kmpiap.kmpIapInstance
import io.github.hyochan.kmpiap.openiap.BillingProgramAndroid
import io.github.hyochan.kmpiap.openiap.InitConnectionConfig
// Enable External Payments during connection initialization
val config = InitConnectionConfig(
enableBillingProgramAndroid = BillingProgramAndroid.ExternalPayments
)
val connected = kmpIapInstance.initConnection(config)
Usage
When making a purchase, provide developerBillingOption to enable the side-by-side choice:
import io.github.hyochan.kmpiap.kmpIapInstance
import io.github.hyochan.kmpiap.requestPurchase
import io.github.hyochan.kmpiap.openiap.BillingProgramAndroid
import io.github.hyochan.kmpiap.openiap.DeveloperBillingLaunchModeAndroid
import io.github.hyochan.kmpiap.openiap.DeveloperBillingOptionParamsAndroid
import kotlinx.coroutines.launch
suspend fun handleExternalPaymentsPurchase(productId: String) {
try {
val purchase = kmpIapInstance.requestPurchase {
android {
skus = listOf(productId)
developerBillingOption = DeveloperBillingOptionParamsAndroid(
billingProgram = BillingProgramAndroid.ExternalPayments,
launchMode = DeveloperBillingLaunchModeAndroid.LaunchInExternalBrowserOrApp,
linkUri = "https://your-site.com/checkout"
)
}
}
println("Purchase completed via Google Play: $purchase")
} catch (e: Exception) {
println("Purchase error: ${e.message}")
}
}
Listening for Developer Billing Selection
When the user selects the developer billing option instead of Google Play:
import io.github.hyochan.kmpiap.kmpIapInstance
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
// Listen for developer provided billing events
scope.launch {
kmpIapInstance.developerProvidedBillingListener.collect { details ->
println("User selected developer billing")
println("External Transaction Token: ${details.externalTransactionToken}")
// IMPORTANT: Process payment on your external site
// The externalTransactionToken must be reported to Google within 24 hours
// After successful payment on your site:
reportToGoogleBackend(details.externalTransactionToken)
}
}
Key Differences from User Choice Billing
| Feature | User Choice Billing | External Payments |
|---|---|---|
| Billing Library | 7.0+ | 8.3.0+ |
| Availability | Eligible regions | Japan only |
| UI | Separate dialog | Side-by-side in purchase dialog |
| Token Source | userChoiceBillingListener | developerProvidedBillingListener |
Complete Example
import io.github.hyochan.kmpiap.KmpIAP
import io.github.hyochan.kmpiap.openiap.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.collect
class ExternalPaymentsManager {
private val kmpIAP = KmpIAP()
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
suspend fun initialize() {
// Initialize connection with External Payments enabled
val config = InitConnectionConfig(
enableBillingProgramAndroid = BillingProgramAndroid.ExternalPayments
)
val connected = kmpIAP.initConnection(config)
if (!connected) {
println("Failed to connect")
return
}
// Listen for developer billing selection
scope.launch {
kmpIAP.developerProvidedBillingListener.collect { details ->
handleDeveloperBilling(details)
}
}
// Listen for regular purchases (when user selects Google Play)
scope.launch {
kmpIAP.purchaseUpdatedListener.collect { purchase ->
handleGooglePlayPurchase(purchase)
}
}
}
suspend fun purchaseWithExternalPaymentsOption(productId: String) {
try {
kmpIAP.requestPurchase {
android {
skus = listOf(productId)
developerBillingOption = DeveloperBillingOptionParamsAndroid(
billingProgram = BillingProgramAndroid.ExternalPayments,
launchMode = DeveloperBillingLaunchModeAndroid.LaunchInExternalBrowserOrApp,
linkUri = "https://your-site.com/checkout?product=$productId"
)
}
}
} catch (e: Exception) {
println("Purchase request failed: ${e.message}")
}
}
private fun handleDeveloperBilling(details: DeveloperProvidedBillingDetailsAndroid) {
println("User selected developer billing")
// Process payment on your external site
// Report token to Google within 24 hours
reportToGoogleBackend(details.externalTransactionToken)
}
private fun handleGooglePlayPurchase(purchase: Purchase) {
println("Purchase completed via Google Play: ${purchase.productId}")
// Handle Google Play purchase normally
}
private fun reportToGoogleBackend(token: String) {
// Report the external transaction token to Google
// This must be done within 24 hours of the transaction
}
}
Complete Cross-Platform Example
See the AlternativeBillingScreen.kt in the example app for a complete implementation:
import io.github.hyochan.kmpiap.kmpIapInstance
import io.github.hyochan.kmpiap.openiap.*
import kotlinx.coroutines.launch
@Composable
fun AlternativeBillingScreen() {
var selectedProduct by remember { mutableStateOf<ProductCommon?>(null) }
var billingProgram by remember {
mutableStateOf(BillingProgramAndroid.ExternalOffer)
}
var externalUrl by remember { mutableStateOf("https://your-site.com") }
LaunchedEffect(Unit) {
// Initialize connection with billing program (v1.3.0+)
val config = if (getPlatformName() == "Android") {
InitConnectionConfig(enableBillingProgramAndroid = billingProgram)
} else null
kmpIapInstance.initConnection(config)
}
// Platform-specific purchase handling
fun handlePurchase(product: ProductCommon) {
scope.launch {
if (getPlatformName() == "iOS") {
// iOS: Use external URL
val result = kmpIapInstance.presentExternalPurchaseLinkIOS(
url = "$externalUrl?product=${product.id}"
)
if (result.success) {
println("Redirected to external checkout")
} else {
println("Error: ${result.error}")
}
} else {
// Android: Handle based on billing program
when (billingProgram) {
BillingProgramAndroid.ExternalOffer -> {
handleAndroidExternalOffer(product)
}
BillingProgramAndroid.UserChoiceBilling -> {
handleAndroidUserChoice(product)
}
BillingProgramAndroid.ExternalPayments -> {
handleAndroidExternalPayments(product)
}
else -> {
println("Billing program not configured")
}
}
}
}
}
// UI implementation...
}
suspend fun handleAndroidAlternativeBillingOnly(product: ProductCommon) {
val isAvailable = kmpIapInstance.checkAlternativeBillingAvailabilityAndroid()
if (!isAvailable) {
println("Alternative billing not available")
return
}
val userAccepted = kmpIapInstance.showAlternativeBillingDialogAndroid()
if (!userAccepted) return
// Process payment with your system...
val token = kmpIapInstance.createAlternativeBillingTokenAndroid()
if (token != null) {
reportToGoogleBackend(token)
}
}
suspend fun handleAndroidUserChoice(product: ProductCommon) {
try {
val purchase = kmpIapInstance.requestPurchase {
android {
skus = listOf(product.id)
}
}
println("Purchase completed via Google Play")
} catch (e: Exception) {
println("Error: ${e.message}")
}
}
Best Practices
General
- Backend Validation: Always validate purchases on your backend server
- Clear Communication: Inform users they're leaving the app for external payment
- Deep Linking: Implement deep links to return users to your app (iOS)
- Error Handling: Handle all error cases gracefully
iOS Specific
- iOS Version Check: Verify iOS 16.0+ before enabling alternative billing
- URL Validation: Ensure external URLs are valid and secure (HTTPS)
- No Purchase Events: Don't rely on
purchaseUpdatedListenerwhen using external URLs - Deep Link Implementation: Crucial for returning users to your app
Android Specific
- 24-Hour Reporting: Report tokens to Google within 24 hours
- Mode Selection: Choose the appropriate mode for your use case
- User Experience: User Choice mode provides better UX but shares revenue with Google
- Backend Integration: Implement proper token reporting to Google Play
Testing
iOS Testing
- Test on real devices running iOS 16.0+
- Verify external URL opens correctly in Safari
- Test deep link return flow
- Ensure StoreKit is configured for alternative billing
Android Testing
- Configure alternative billing in Google Play Console
- Test both billing modes separately
- Verify token generation and reporting
- Test user choice dialog behavior
Troubleshooting
iOS Issues
"Feature not supported"
- Ensure iOS 16.0 or later
- Verify external purchase entitlement is approved
"External URL not opening"
- Check URL format (must be valid HTTPS)
- Verify entitlements are properly configured
"User stuck on external site"
- Implement deep linking to return to app
- Test deep link handling
Android Issues
"Alternative billing not available"
- Verify Google Play approval
- Check device and Play Store version
- Ensure billing mode is configured
"Token creation failed"
- Verify billing mode configuration
- Ensure user completed info dialog
- Check Google Play Console settings
"User choice dialog not showing"
- Verify
BillingProgramAndroid.UserChoiceBillingis set viaenableBillingProgramAndroid - Ensure Google Play configuration is correct
- Check device compatibility (Billing Library 7.0+ required)
Platform Requirements
- iOS: iOS 16.0+ for external purchase URLs
- Android: Google Play Billing Library 8.0+ with alternative billing enabled
- Approval: Both platforms require approval for alternative billing features
API Reference
iOS APIs
presentExternalPurchaseLinkIOS(url: String): ExternalPurchaseLinkResultIOScanPresentExternalPurchaseNoticeIOS(): BooleanpresentExternalPurchaseNoticeSheetIOS(): ExternalPurchaseNoticeResultIOS
Android APIs
checkAlternativeBillingAvailabilityAndroid(): BooleanshowAlternativeBillingDialogAndroid(): BooleancreateAlternativeBillingTokenAndroid(): String?userChoiceBillingListener: Flow<UserChoiceBillingDetails>developerProvidedBillingListener: Flow<DeveloperProvidedBillingDetailsAndroid>(8.3.0+)
InitConnectionConfig Options
alternativeBillingModeAndroid: AlternativeBillingModeAndroid- Deprecated. UseenableBillingProgramAndroidinsteadenableBillingProgramAndroid: BillingProgramAndroid- Enable a billing program (7.0+ for UserChoiceBilling, 8.2.0+ for others)
BillingProgramAndroid Values (v1.3.0)
| Value | Description | Min Version |
|---|---|---|
Unspecified | No billing program | - |
UserChoiceBilling | User Choice Billing (replaces deprecated AlternativeBillingModeAndroid.UserChoice) | 7.0+ |
ExternalContentLink | External content link programs | 8.2.0+ |
ExternalOffer | External offer programs (replaces deprecated AlternativeBillingModeAndroid.AlternativeOnly) | 8.2.0+ |
ExternalPayments | Developer Provided Billing (Japan only) | 8.3.0+ |
Android Types (External Payments)
BillingProgramAndroid.ExternalPayments- External Payments program type (8.3.0+)DeveloperBillingOptionParamsAndroid- Parameters for enabling External Payments in purchase flowDeveloperBillingLaunchModeAndroid- How to launch the external payment linkDeveloperProvidedBillingDetailsAndroid- ContainsexternalTransactionTokenwhen user selects developer billing
