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​