Skip to main content
Version: 3.4 (Current)

Alternative Billing

IAPKit - In-App Purchase Solution

This guide explains how to implement alternative billing functionality in your app using expo-iap, allowing you to use external payment systems alongside or instead of the App Store/Google Play billing.

Official Documentation

Apple (iOS)

Configuration: Configure iOS alternative billing in your app.config.ts:

export default {
// ... other config
plugins: [
[
'expo-iap',
{
iosAlternativeBilling: {
// Required: Countries where external purchases are supported
countries: ['kr', 'nl', 'de', 'fr'], // ISO 3166-1 alpha-2

// Optional: Enable external purchase link entitlement
enableExternalPurchaseLink: true,
},
},
],
],
};

Google Play (Android)

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)
Platform Approval Required

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 URLs)

On iOS, alternative billing works by redirecting users to an external website where they complete the purchase.

Configuration

Configure iOS alternative billing in your app.config.ts:

export default {
// ... other config
plugins: [
[
'expo-iap',
{
iosAlternativeBilling: {
// Required: Countries where external purchases are supported (ISO 3166-1 alpha-2)
countries: ['kr', 'nl', 'de', 'fr', 'it', 'es'],

// Optional: External purchase URLs per country (iOS 15.4+)
links: {
kr: 'https://your-site.com/kr/checkout',
nl: 'https://your-site.com/nl/checkout',
de: 'https://your-site.com/de/checkout',
},

// Optional: Multiple URLs per country (iOS 17.5+, up to 5)
multiLinks: {
fr: [
'https://your-site.com/fr',
'https://your-site.com/global-sale',
],
it: ['https://your-site.com/global-sale'],
},

// Optional: Custom link regions (iOS 18.1+)
customLinkRegions: ['de', 'fr', 'nl'],

// Optional: Streaming regions for music apps (iOS 18.2+)
streamingLinkRegions: ['at', 'de', 'fr', 'nl', 'is', 'no'],

// Enable external purchase link entitlement
enableExternalPurchaseLink: true,

// Enable streaming entitlement (music apps only)
enableExternalPurchaseLinkStreaming: false,
},
},
],
],
};

This automatically adds the required configuration to your iOS app:

Entitlements:

<plist>
<dict>
<!-- Automatically added when countries are specified -->
<key>com.apple.developer.storekit.external-purchase</key>
<true/>

<!-- Added when enableExternalPurchaseLink is true -->
<key>com.apple.developer.storekit.external-purchase-link</key>
<true/>

<!-- Added when enableExternalPurchaseLinkStreaming is true -->
<key>com.apple.developer.storekit.external-purchase-link-streaming</key>
<true/>
</dict>
</plist>

Info.plist:

<plist>
<dict>
<!-- Countries where external purchases are supported -->
<key>SKExternalPurchase</key>
<array>
<string>kr</string>
<string>nl</string>
<string>de</string>
</array>

<!-- External purchase URLs (optional) -->
<key>SKExternalPurchaseLink</key>
<dict>
<key>kr</key>
<string>https://your-site.com/kr/checkout</string>
</dict>

<!-- Multiple URLs per country (optional) -->
<key>SKExternalPurchaseMultiLink</key>
<dict>
<key>fr</key>
<array>
<string>https://your-site.com/fr</string>
<string>https://your-site.com/global-sale</string>
</array>
</dict>
</dict>
</plist>
Requirements
  • Approval Required: You must obtain approval from Apple to use external purchase features
  • URL Format: URLs must use HTTPS, have no query parameters, and be 1,000 characters or fewer
  • Link Limits:
    • Music streaming apps: up to 5 links per country (EU + Iceland, Norway)
    • Other apps: 1 link per country
  • Supported Regions: Different features support different regions (EU, US, etc.)

See External Purchase Link Entitlement for details.

Basic Usage

import {presentExternalPurchaseLinkIOS} from 'expo-iap';

const purchaseWithExternalUrl = async () => {
try {
const result = await presentExternalPurchaseLinkIOS('https://your-payment-site.com/checkout');

if (result.success) {
// User was redirected to the external URL
// No onPurchaseUpdated callback will fire
console.log('User redirected to external payment site');
} else if (result.error) {
console.error('External purchase link error:', result.error);
}
} catch (error) {
console.error('Alternative billing error:', error);
}
};

Important Notes

  • iOS 16.0+ Required: External purchase links only work on iOS 16.0 and later
  • No Purchase Callback: The onPurchaseUpdated callback will 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 {presentExternalPurchaseLinkIOS} from 'expo-iap';
import {Platform, Alert} from 'react-native';

function MyComponent() {
const handleAlternativeBillingPurchase = async (externalUrl: string) => {
if (Platform.OS !== 'ios') return;

try {
const result = await presentExternalPurchaseLinkIOS(externalUrl);

if (result.success) {
Alert.alert(
'Redirected',
'Complete your purchase on the external website. You will be redirected back to the app.',
);
} else if (result.error) {
Alert.alert('Error', result.error);
}
} catch (error: any) {
if (error.code === 'user-cancelled') {
console.log('User cancelled');
} else {
Alert.alert('Error', error.message);
}
}
};

// ... rest of component
}

Android Alternative Billing

Android supports three alternative billing modes:

  1. Alternative Billing Only: Users can ONLY use your payment system
  2. User Choice Billing: Users choose between Google Play or your payment system
  3. External Payments (8.3.0+, Japan only): Side-by-side choice during purchase

Mode 1: Alternative Billing Only

This mode requires a manual 3-step flow:

import {
useIAP,
checkAlternativeBillingAvailabilityAndroid,
showAlternativeBillingDialogAndroid,
createAlternativeBillingTokenAndroid,
} from 'expo-iap';

const handleAlternativeBillingOnly = async (productId: string) => {
try {
// Step 1: Check availability
const isAvailable = await checkAlternativeBillingAvailabilityAndroid();
if (!isAvailable) {
Alert.alert('Error', 'Alternative billing not available');
return;
}

// Step 2: Show information dialog
const userAccepted = await showAlternativeBillingDialogAndroid();
if (!userAccepted) {
console.log('User declined');
return;
}

// Step 2.5: Process payment with your payment system
// ... your payment processing logic here ...

// Step 3: Create reporting token (after successful payment)
const token = await createAlternativeBillingTokenAndroid(productId);

// Step 4: Report token to Google Play backend within 24 hours
await reportToGoogleBackend(token);

console.log('Alternative billing completed');
} catch (error) {
console.error('Alternative billing error:', error);
}
};

Mode 2: User Choice Billing

With user choice, Google automatically shows a selection dialog:

import {useIAP, requestPurchase} from 'expo-iap';

// Initialize with user choice mode (new API)
const {connected, products, fetchProducts} = useIAP({
enableBillingProgramAndroid: 'user-choice-billing',
onPurchaseSuccess: (purchase) => {
// This fires if user selects Google Play
console.log('Google Play purchase:', purchase);
},
});

const handleUserChoicePurchase = async (productId: string) => {
try {
// Google will show selection dialog automatically
await requestPurchase({
request: {
google: {
skus: [productId],
},
},
type: 'in-app',
});

// If user selects Google Play: onPurchaseSuccess callback fires
// If user selects alternative: No callback (manual flow required)
} catch (error) {
console.error('Purchase error:', error);
}
};

Mode 3: External Payments (Japan Only)

Billing Library 8.3.0+ Required

External Payments requires Google Play Billing Library 8.3.0 or higher and is currently only available in Japan.

External Payments presents a side-by-side choice between Google Play Billing and your external payment option directly in the purchase dialog.

import {useEffect} from 'react';
import {
initConnection,
isBillingProgramAvailableAndroid,
developerProvidedBillingListenerAndroid,
requestPurchase,
} from 'expo-iap';
import {Platform} from 'react-native';

function ExternalPaymentsComponent() {
useEffect(() => {
if (Platform.OS !== 'android') return;

const initialize = async () => {
// Initialize with External Payments program
await initConnection({
enableBillingProgramAndroid: 'external-payments',
});

// Check availability (Japan only)
const result = await isBillingProgramAvailableAndroid('external-payments');
if (!result.isAvailable) {
console.log('External Payments not available');
return;
}

// Set up listener for when user selects developer billing
const subscription = developerProvidedBillingListenerAndroid(
async (details) => {
console.log('User selected developer billing');

// Process payment with your gateway
await processPayment(details.externalTransactionToken);

// Report to Google within 24 hours
await reportToGoogle(details.externalTransactionToken);
},
);

return () => subscription.remove();
};

const cleanup = initialize();
return () => {
cleanup.then((fn) => fn?.());
};
}, []);

const purchaseWithExternalPayments = async (sku: string) => {
// Request purchase with developer billing option
await requestPurchase({
request: {
google: {
skus: [sku],
developerBillingOption: {
billingProgram: 'external-payments',
linkUri: 'https://your-payment-site.com/checkout',
launchMode: 'launch-in-external-browser-or-app',
},
},
},
type: 'in-app',
});
};

// ... rest of component
}

Key Differences from User Choice Billing

FeatureUser Choice BillingExternal Payments
Billing Library7.0+8.3.0+
AvailabilityEligible regionsJapan only
When presentedAfter initConnection()During requestPurchase()
UISeparate dialogSide-by-side choice in purchase dialog
ListeneruserChoiceBillingListenerAndroiddeveloperProvidedBillingListenerAndroid

Configuring Billing Program

Set the billing program when initializing the connection:

import {useIAP} from 'expo-iap';

const {connected} = useIAP({
// Available programs: 'user-choice-billing', 'external-offer', 'external-payments', 'external-content-link'
enableBillingProgramAndroid: 'user-choice-billing',
});

Or use the root API:

import {initConnection, type BillingProgramAndroid} from 'expo-iap';

await initConnection({
enableBillingProgramAndroid: 'external-offer',
});

For External Payments:

import {initConnection} from 'expo-iap';

await initConnection({
enableBillingProgramAndroid: 'external-payments',
});

Complete Cross-Platform Example

import {Platform, Alert} from 'react-native';
import {
useIAP,
requestPurchase,
presentExternalPurchaseLinkIOS,
isBillingProgramAvailableAndroid,
launchExternalLinkAndroid,
createBillingProgramReportingDetailsAndroid,
type BillingProgramAndroid,
} from 'expo-iap';

function AlternativeBillingComponent() {
const [billingProgram, setBillingProgram] =
useState<BillingProgramAndroid>('external-offer');

const {connected, products, fetchProducts} = useIAP({
enableBillingProgramAndroid:
Platform.OS === 'android' ? billingProgram : undefined,
onPurchaseSuccess: (purchase) => {
console.log('Purchase successful:', purchase);
},
onPurchaseError: (error) => {
console.error('Purchase error:', error);
},
});

const handlePurchase = async (productId: string) => {
if (Platform.OS === 'ios') {
// iOS: External URL using StoreKit External Purchase Link API
const result = await presentExternalPurchaseLinkIOS('https://your-payment-site.com/checkout');
if (result.success) {
Alert.alert('Redirected', 'Complete purchase on external website');
}
} else if (Platform.OS === 'android') {
if (billingProgram === 'user-choice-billing') {
// Android: User Choice Billing - Google shows selection dialog
await requestPurchase({
request: {
google: {
skus: [productId],
},
},
type: 'in-app',
});
} else {
// Android: Billing Programs API (external-offer, external-payments, etc.)
const availability = await isBillingProgramAvailableAndroid(billingProgram);
if (!availability.isAvailable) {
Alert.alert('Error', 'Billing program not available');
return;
}

await launchExternalLinkAndroid({
billingProgram,
launchMode: 'launch-in-external-browser-or-app',
linkType: 'link-to-digital-content-offer',
linkUri: `https://your-payment-site.com/purchase/${productId}`,
});

const details = await createBillingProgramReportingDetailsAndroid(billingProgram);
// Report token to Google within 24 hours
console.log('Token:', details.externalTransactionToken);
}
}
};

// ... rest of component
}

Best Practices

General

  1. Backend Validation: Always validate purchases on your backend server
  2. Clear Communication: Inform users they're leaving the app for external payment
  3. Deep Linking: Implement deep links to return users to your app (iOS)
  4. Error Handling: Handle all error cases gracefully

iOS Specific

  1. iOS Version Check: Verify iOS 16.0+ before enabling alternative billing
  2. URL Validation: Ensure external URLs are valid and secure (HTTPS)
  3. No Purchase Events: Don't rely on onPurchaseUpdated when using external URLs
  4. Deep Link Implementation: Crucial for returning users to your app

Android Specific

  1. 24-Hour Reporting: Report tokens to Google within 24 hours
  2. Mode Selection: Choose the appropriate mode for your use case
  3. User Experience: User Choice mode provides better UX but shares revenue with Google
  4. Backend Integration: Implement proper token reporting to Google Play

Testing

iOS Testing

  1. Test on real devices running iOS 16.0+
  2. Verify external URL opens correctly in Safari
  3. Test deep link return flow
  4. Ensure StoreKit is configured for alternative billing

Android Testing

  1. Configure alternative billing in Google Play Console
  2. Test both billing modes separately
  3. Verify token generation and reporting
  4. 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)
  • Use presentExternalPurchaseLinkIOS() for iOS external links

"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 product ID is correct
  • Check billing mode configuration
  • Ensure user completed info dialog

"User choice dialog not showing"

  • Verify enableBillingProgramAndroid: 'user-choice-billing'
  • Check Google Play configuration

"External Payments not available"

  • External Payments requires Billing Library 8.3.0+
  • Currently only available in Japan
  • Verify enableBillingProgramAndroid: 'external-payments' is set

Platform Requirements

  • iOS: iOS 16.0+ for external purchase URLs
  • Android: Google Play Billing Library 5.0+ with alternative billing enabled (8.3.0+ for External Payments)
  • Approval: Both platforms require approval for alternative billing features

See Also