Back to the RevenueCat homepage
RevenueCat SDKGoogle Play Billing

Chapter 18: Alternative Billing and External Offers

Google Play's billing system has historically been the only way to sell digital goods in Play distributed apps. That changed. Regulatory pressure in South Korea, the European Economic Area (EEA), India, and the United States has pushed Google to open up alternative paths for processing payments. The Play Billing Library now offers several programs that let you present users with choices beyond Google Play's standard payment flow, or even handle billing entirely outside of it.

This chapter covers every alternative billing and external linking program available in PBL 8.x. You will learn how each program works, which APIs to call, what regions they apply to, and how to report transactions back to Google. These APIs evolve quickly, so pay attention to which PBL version introduced each feature.

When and Why Alternative Billing Applies

Alternative billing exists because regulators in several countries determined that requiring a single payment system in an app store constitutes anticompetitive behavior. Google responded by creating programs that give developers more flexibility in how they charge users for digital goods.

There are several distinct programs, each with different rules:

  • User choice billing: You offer both Google Play's billing and your own billing system. The user picks which one to use at purchase time. Google renders a choice screen.
  • Alternative billing only: You bypass Google Play's billing entirely and use only your own payment system. Google shows an information dialog to the user explaining the situation.
  • External offers: You link users out of your app to a website where they can find deals on digital content or download apps.
  • External content links: You direct users to an external website for digital content, without processing the payment inside the app.
  • External payment links: You present users with a choice between Google Play billing and a developer provided billing option that opens an external payment page.

Why would you use these? The most common reason is to reduce service fees. Google charges a 4% lower service fee on transactions processed through an alternative billing system. For high volume apps, that adds up. You might also want to offer payment methods Google does not support, consolidate billing across platforms, or comply with regional regulations that require you to offer alternatives.

The trade off is real: you take on more responsibility. You handle payment processing, fraud detection, refunds, and compliance yourself. You also must report every external transaction back to Google within 24 hours using the externaltransactions API. If you miss that window, you risk policy violations.

Alternative Billing with User Choice

User choice billing gives your users a screen where they can pick between Google Play's billing and your own. Google renders and manages this choice screen, so you do not need to build the UI yourself.

Setting Up the BillingClient

To enable user choice billing, call enableUserChoiceBilling() on the BillingClient.Builder and pass a UserChoiceBillingListener. This listener fires when the user selects your alternative billing system instead of Google Play.

kotlin
val userChoiceBillingListener =
    UserChoiceBillingListener { userChoiceDetails ->
        // User chose your billing system.
        // Send the token to your backend.
        val token = userChoiceDetails
            .externalTransactionToken
        val products = userChoiceDetails.products
        backend.startExternalPurchase(token, products)
    }

val billingClient = BillingClient.newBuilder(context)
    .setListener(purchasesUpdatedListener)
    .enablePendingPurchases(
        PendingPurchasesParams.newBuilder().build()
    )
    .enableUserChoiceBilling(userChoiceBillingListener)
    .build()

You still set a PurchasesUpdatedListener because when the user picks Google Play's billing, the standard purchase flow runs and results come through that listener as usual.

The Choice Screen Flow

When you call launchBillingFlow(), Google Play checks whether the user is in a supported country and whether you called enableUserChoiceBilling(). If both conditions are met, Google shows the choice screen. If the user is not in a supported country, the standard Google Play purchase dialog appears instead, and no choice screen is shown.

The flow has two outcomes:

  1. User picks Google Play: The purchase proceeds normally. Your PurchasesUpdatedListener.onPurchasesUpdated() callback fires with the BillingResult and purchases.
  2. User picks your billing system: The UserChoiceBillingListener.userSelectedAlternativeBilling() callback fires with a UserChoiceDetails object. This object contains the list of products the user wants to buy and an externalTransactionToken that you must send to your backend.

After the user picks your billing, you are responsible for the entire payment flow. Collect payment through your system, then report the transaction to Google's externaltransactions API within 24 hours using the token.

Subscription Upgrades and Downgrades

When a user who originally purchased through your alternative billing system wants to change their subscription plan, you can skip the choice screen by using setOriginalExternalTransactionId():

kotlin
val billingFlowParams = BillingFlowParams.newBuilder()
    .setProductDetailsParamsList(
        listOf(
            BillingFlowParams.ProductDetailsParams
                .newBuilder()
                .setProductDetails(newPlanDetails)
                .setOfferToken(newOfferToken)
                .build()
        )
    )
    .setSubscriptionUpdateParams(
        BillingFlowParams.SubscriptionUpdateParams
            .newBuilder()
            .setOriginalExternalTransactionId(
                originalExternalTxId
            )
            .build()
    )
    .build()

This tells Google Play that the user already chose alternative billing for the original subscription, so there is no need to ask again. A new externalTransactionToken is generated for the upgrade or downgrade transaction.

Version History

The user choice billing concept was introduced in PBL 5.2 under the name enableAlternativeBilling with an AlternativeBillingListener. In PBL 6.1, these were renamed to enableUserChoiceBilling and UserChoiceBillingListener for clarity. The old names still exist for backwards compatibility but are deprecated. In PBL 8.0, UserChoiceDetails replaced the deprecated AlternativeChoiceDetails. Use the newer names in any new integration.

Alternative Billing Only

Alternative billing only means you do not offer Google Play's billing at all. The user pays exclusively through your payment system. Google still requires you to show an information dialog that tells the user they are being billed outside of Google Play.

Setting Up the BillingClient

The setup is simpler than user choice billing. You call enableAlternativeBillingOnly() and do not need a PurchasesUpdatedListener, since no purchases will flow through Google Play.

kotlin
val billingClient = BillingClient.newBuilder(context)
    .enableAlternativeBillingOnly()
    .build()

Checking Availability

Before starting a purchase, verify that alternative billing only is available for the current user:

kotlin
billingClient.isAlternativeBillingOnlyAvailableAsync(
    object : AlternativeBillingOnlyAvailabilityListener {
        override fun onAlternativeBillingOnlyAvailabilityResponse(
            billingResult: BillingResult
        ) {
            if (billingResult.responseCode !=
                BillingResponseCode.OK) {
                // Not available. Fall back or show error.
                return
            }
            // Proceed with alternative billing.
        }
    }
)

A BILLING_UNAVAILABLE response means the user or your account is not eligible. Check your Play Console enrollment status.

Showing the Information Dialog

Before each transaction, you must call showAlternativeBillingOnlyInformationDialog(). This shows a Google managed dialog that informs the user they are paying through your system, not Google Play. The dialog content differs slightly between regions (US vs. EEA messaging).

kotlin
billingClient
    .showAlternativeBillingOnlyInformationDialog(
        activity,
        object :
            AlternativeBillingOnlyInformationDialogListener {
            override fun onAlternativeBillingOnlyInformationDialogResponse(
                billingResult: BillingResult
            ) {
                when (billingResult.responseCode) {
                    BillingResponseCode.OK ->
                        proceedWithPurchase()
                    BillingResponseCode.USER_CANCELED ->
                        // Show dialog again next attempt
                        Unit
                    else -> handleError(billingResult)
                }
            }
        }
    )

The dialog typically does not show again once the user has acknowledged it on the same device. However, if the device cache is cleared, it will appear again. If the response is USER_CANCELED, call the dialog again on the next purchase attempt.

Generating the Transaction Token

Before completing a transaction, call createAlternativeBillingOnlyReportingDetailsAsync() to get an externalTransactionToken:

kotlin
billingClient
    .createAlternativeBillingOnlyReportingDetailsAsync(
        object :
            AlternativeBillingOnlyReportingDetailsListener {
            override fun onAlternativeBillingOnlyTokenResponse(
                billingResult: BillingResult,
                details:
                    AlternativeBillingOnlyReportingDetails?
            ) {
                if (billingResult.responseCode !=
                    BillingResponseCode.OK) return
                val token =
                    details?.externalTransactionToken
                // Send to your backend
            }
        }
    )

Send this token to your backend. After the user completes payment through your system, report the transaction to Google's externaltransactions API within 24 hours.

Alternative billing only requires PBL 6.1 or higher. The APIs work in PBL 8.x without changes.

Regional Eligibility

Not every program is available everywhere. Regional regulations drive which options you can offer and to whom.

South Korea

South Korea was the first country to require alternative billing options, driven by the Telecommunications Business Act amendment in 2021. Developers can offer user choice billing to mobile and tablet users in South Korea. The service fee is reduced by 4% for transactions where users pay through the alternative billing system. Both gaming and non gaming apps are eligible. South Korea is not part of the user choice billing pilot; it operates under its own regulatory framework.

European Economic Area (EEA)

The EEA includes all EU member states plus Iceland, Liechtenstein, and Norway. As of March 2024, both gaming and non gaming apps are eligible for user choice billing and alternative billing only when serving users in the EEA. The EEA also has its own external offers program with a tiered service fee model. The fee structure includes a required Tier 1 ongoing service fee (10% on transactions) and an optional Tier 2 fee that covers additional Play services. Developers in the EEA also have access to external content links and external payment links.

India

All developers can offer an alternative billing system alongside Google Play's billing for Indian users making in app purchases on mobile phones and tablets. The 4% service fee reduction applies here as well. When reporting transactions for Indian users, you must include the administrativeArea (state or province) in the userTaxAddress because tax rates vary by state.

United States

The US programs emerged from the Epic Games v. Google settlement. Developers of mobile and tablet games can offer user choice billing to US users. The external content links program (PBL 8.2+) is available for apps serving US users, allowing you to link out to your website. The external payment links program (PBL 8.3+) enables a choice screen with a developer provided billing option for eligible apps. Google announced updated policies in December 2025 that affect these programs, so check the Play Console for the latest requirements and enrollment deadlines.

Checking Eligibility at Runtime

You do not need to hard code country lists. The PBL APIs handle eligibility checks for you. When you call launchBillingFlow() with user choice billing enabled, Google Play only shows the choice screen if the user is in a supported country. The isBillingProgramAvailableAsync() method for external programs returns BILLING_UNAVAILABLE if the user is not eligible. Let the APIs do the filtering.

External Offers: Configuration and In App Integration

The external offers program lets you direct users outside your app to a website where you offer deals on digital content or app downloads. This program was originally available through dedicated APIs (enableExternalOffer, isExternalOfferAvailableAsync), but PBL 8.2 replaced those with the unified enableBillingProgram() API.

Play Console Configuration

Before writing any code, enroll in the external offers program through the Play Console. You will need to:

  1. Accept the program terms
  2. Register any external URLs you plan to link to
  3. Provide a subscription management link if you offer subscriptions
  4. Select which countries to participate in

In App Integration (PBL 8.2+)

Initialize the BillingClient with BillingProgram.EXTERNAL_OFFER:

kotlin
val billingClient = BillingClient.newBuilder(context)
    .enableBillingProgram(
        BillingProgram.EXTERNAL_OFFER
    )
    .build()

Check whether the user is eligible:

kotlin
billingClient.isBillingProgramAvailableAsync(
    BillingProgram.EXTERNAL_OFFER,
    object : BillingProgramAvailabilityListener {
        override fun onBillingProgramAvailabilityResponse(
            billingResult: BillingResult,
            details: BillingProgramAvailabilityDetails
        ) {
            if (billingResult.responseCode !=
                BillingResponseCode.OK) return
            // External offers available for this user
        }
    }
)

Generate a token and launch the external link:

kotlin
val params = BillingProgramReportingDetailsParams
    .newBuilder()
    .setBillingProgram(BillingProgram.EXTERNAL_OFFER)
    .build()

billingClient
    .createBillingProgramReportingDetailsAsync(
        params,
        object : BillingProgramReportingDetailsListener {
            override fun onCreateBillingProgramReportingDetailsResponse(
                billingResult: BillingResult,
                details: BillingProgramReportingDetails?
            ) {
                val token =
                    details?.externalTransactionToken
                // Persist token, then launch link
                launchOffer(token)
            }
        }
    )

To actually direct the user to your external offer, use launchExternalLink():

kotlin
val linkParams = LaunchExternalLinkParams.newBuilder()
    .setBillingProgram(BillingProgram.EXTERNAL_OFFER)
    .setLinkUri(Uri.parse("https://myapp.com/offer"))
    .setLaunchMode(
        LaunchExternalLinkParams.LaunchMode
            .LAUNCH_IN_EXTERNAL_BROWSER_OR_APP
    )
    .build()

billingClient.launchExternalLink(
    activity, linkParams, launchListener
)

The LAUNCH_IN_EXTERNAL_BROWSER_OR_APP mode lets Google Play handle opening the URL. If you need to control how the link opens (for example, in a WebView or a specific browser), use CALLER_WILL_LAUNCH_LINK instead.

Note that PBL 8.2.0 had a bug in isBillingProgramAvailableAsync() and createBillingProgramReportingDetailsAsync(). Use PBL 8.2.1 or later.

External content links allow you to direct users outside your app to a website that offers digital content or app downloads. This program is currently available for apps serving users in the US.

How It Differs from External Offers

External content links and external offers use the same APIs but different BillingProgram constants. The distinction is primarily a policy and program enrollment difference. External content links use BillingProgram.EXTERNAL_CONTENT_LINK.

Integration

The integration pattern is identical to external offers, with the constant swapped:

kotlin
val billingClient = BillingClient.newBuilder(context)
    .enableBillingProgram(
        BillingProgram.EXTERNAL_CONTENT_LINK
    )
    .build()

Check availability with isBillingProgramAvailableAsync():

kotlin
billingClient.isBillingProgramAvailableAsync(
    BillingProgram.EXTERNAL_CONTENT_LINK,
    object : BillingProgramAvailabilityListener {
        override fun onBillingProgramAvailabilityResponse(
            billingProgram: Int,
            billingResult: BillingResult
        ) {
            if (billingResult.responseCode !=
                BillingResponseCode.OK) return
            // External content links available
        }
    }
)

Generate the transaction token and launch the link using the same createBillingProgramReportingDetailsAsync() and launchExternalLink() methods shown in the external offers section, replacing the BillingProgram constant with EXTERNAL_CONTENT_LINK.

One important detail: for app download links, the target URL must be registered and approved in the Play Developer Console. Do not include personally identifiable information in the URI. Perform any cleanup operations before launching the link, because users may not return to your app after the external browser opens.

External payment links represent the newest alternative billing program. Introduced in PBL 8.3.0 (released December 2025), this program lets you present users with a choice between Google Play's billing system and a developer provided billing option that opens an external payment page. Unlike user choice billing, which predates PBL 8, external payment links use a more structured API with dedicated parameter classes.

New APIs in PBL 8.3

PBL 8.3.0 introduced several new classes:

  • BillingProgram.EXTERNAL_PAYMENTS: The program constant
  • EnableBillingProgramParams: Configuration for enabling the program
  • DeveloperBillingOptionParams: Parameters for your external payment option
  • DeveloperProvidedBillingListener: Callback for when the user picks your billing
  • DeveloperProvidedBillingDetails: Details passed to your listener

Setting Up the BillingClient

The setup uses EnableBillingProgramParams to bundle the program type and listener:

kotlin
val devBillingListener =
    DeveloperProvidedBillingListener { details ->
        // User chose your payment option.
        // Process with your billing system.
        backend.handleExternalPayment(details)
    }

val billingClient = BillingClient.newBuilder(context)
    .setListener(purchasesUpdatedListener)
    .enablePendingPurchases(
        PendingPurchasesParams.newBuilder().build()
    )
    .enableBillingProgram(
        EnableBillingProgramParams.newBuilder()
            .setBillingProgram(
                BillingProgram.EXTERNAL_PAYMENTS
            )
            .setDeveloperProvidedBillingListener(
                devBillingListener
            )
            .build()
    )
    .build()

You still provide a PurchasesUpdatedListener because users who choose Google Play billing go through the standard flow.

Checking Availability and Generating Tokens

These steps mirror the pattern from external offers and external content links:

kotlin
billingClient.isBillingProgramAvailableAsync(
    BillingProgram.EXTERNAL_PAYMENTS,
    object : BillingProgramAvailabilityListener {
        override fun onBillingProgramAvailabilityResponse(
            billingProgram: Int,
            billingResult: BillingResult
        ) {
            if (billingResult.responseCode !=
                BillingResponseCode.OK) return
            // External payments available
        }
    }
)

Generate a token immediately before launching the billing flow:

kotlin
val params = BillingProgramReportingDetailsParams
    .newBuilder()
    .setBillingProgram(BillingProgram.EXTERNAL_PAYMENTS)
    .build()

billingClient
    .createBillingProgramReportingDetailsAsync(params,
        object : BillingProgramReportingDetailsListener {
            override fun onCreateBillingProgramReportingDetailsResponse(
                billingResult: BillingResult,
                details: BillingProgramReportingDetails?
            ) {
                val token =
                    details?.externalTransactionToken
                // Store token, build billing flow
            }
        }
    )

Launching the Billing Flow with a Developer Option

When you launch the billing flow, attach a DeveloperBillingOptionParams to present your payment option alongside Google Play's:

kotlin
val devBillingOption = DeveloperBillingOptionParams
    .newBuilder()
    .setBillingProgram(BillingProgram.EXTERNAL_PAYMENTS)
    .setLinkUri(
        Uri.parse("https://myapp.com/checkout")
    )
    .setLaunchMode(
        DeveloperBillingOptionParams.LaunchMode
            .LAUNCH_IN_EXTERNAL_BROWSER_OR_APP
    )
    .build()

Include this in your BillingFlowParams alongside the product details. Google Play renders a choice screen. If the user picks Google Play, the standard flow runs. If they pick your option, the DeveloperProvidedBillingListener fires.

When the Choice Screen Appears

The choice screen only appears when all three conditions are met: the user is in a supported country, you enabled external payments on the BillingClient, and you provided DeveloperBillingOptionParams in the billing flow. If any condition is missing, the standard Google Play billing dialog shows instead.

External Transaction Tokens and IDs

Every alternative billing and external linking program requires you to generate transaction tokens and report transactions. Understanding the difference between tokens and IDs is important.

External Transaction Token

The externalTransactionToken is a string generated by Google Play through the PBL. You obtain it by calling one of these methods, depending on the program:

  • createAlternativeBillingOnlyReportingDetailsAsync() for alternative billing only
  • createBillingProgramReportingDetailsAsync() for external offers, external content links, and external payment links (PBL 8.2+)

For user choice billing, the token comes directly from UserChoiceDetails.externalTransactionToken when the user selects alternative billing.

The token ties the transaction to a specific user and device context. You send it to your backend and include it when reporting the initial transaction to Google's API. Generate a new token before each transaction. Do not cache or reuse tokens across different transactions.

External Transaction ID

The externalTransactionId is a string you generate yourself. It uniquely identifies each transaction in your system. You provide it when reporting transactions to the externaltransactions API. For subscription renewals, each renewal gets its own externalTransactionId, but references the initial transaction's ID via the initialExternalTransactionId field.

Do not include personally identifiable information in the externalTransactionId.

Generating Tokens with the Unified API (PBL 8.2+)

For programs that use createBillingProgramReportingDetailsAsync(), the call looks the same regardless of the program. Only the BillingProgram constant changes:

kotlin
val params = BillingProgramReportingDetailsParams
    .newBuilder()
    .setBillingProgram(
        BillingProgram.EXTERNAL_OFFER
        // or EXTERNAL_CONTENT_LINK
        // or EXTERNAL_PAYMENTS
    )
    .build()

billingClient
    .createBillingProgramReportingDetailsAsync(
        params, reportingDetailsListener
    )

This is one of the cleanest parts of PBL 8.2's redesign: a single method handles token generation for all external programs.

Backend Integration for Outside GPB Transactions

Once you collect payment outside of Google Play, you must report it to Google. This is not optional. Google uses these reports to calculate service fees, track compliance, and maintain records.

The externaltransactions API

All external transactions are reported through the Google Play Developer API's externaltransactions resource at:

kotlin
POST /androidpublisher/v3/applications/{packageName}/externalTransactions

You provide the externalTransactionId as a query parameter and the transaction details in the request body.

Reporting an Initial Purchase

For a new subscription purchased through your billing system:

json
{
  "originalPreTaxAmount": {
    "priceMicros": "4990000",
    "currency": "USD"
  },
  "originalTaxAmount": {
    "priceMicros": "449100",
    "currency": "USD"
  },
  "transactionTime": "2026-01-15T10:30:00Z",
  "recurringTransaction": {
    "externalTransactionToken": "token_from_pbl",
    "externalSubscription": {
      "subscriptionType": "RECURRING"
    }
  },
  "userTaxAddress": {
    "regionCode": "US"
  }
}

Prices use priceMicros, which represent one millionth of the currency unit. Multiply the price by 1,000,000: $4.99 becomes 4990000. Double check your math here, as incorrect values will cause invoicing problems.

Reporting Renewals

For subscription renewals, use a new externalTransactionId and reference the original:

json
{
  "originalPreTaxAmount": {
    "priceMicros": "4990000",
    "currency": "USD"
  },
  "originalTaxAmount": {
    "priceMicros": "449000",
    "currency": "USD"
  },
  "transactionTime": "2026-02-15T10:30:00Z",
  "recurringTransaction": {
    "initialExternalTransactionId": "first-txn-id",
    "externalSubscription": {
      "subscriptionType": "RECURRING"
    }
  },
  "userTaxAddress": {
    "regionCode": "US"
  }
}

Notice the renewal does not include the externalTransactionToken. Only the initial transaction uses the token. Renewals reference the initial transaction by its externalTransactionId.

Reporting Refunds

Report refunds by calling the refund endpoint on the specific externalTransactionId:

kotlin
POST /androidpublisher/v3/applications/{packageName}/externalTransactions/{externalTransactionId}:refund

You can report full or partial refunds. For partial refunds, specify the pre tax amount being refunded.

Timing

Report transactions within 24 hours of the purchase completing. For subscriptions, report each renewal within 24 hours of the renewal charge succeeding. Late reporting can result in policy violations.

API Quotas

The externaltransactions API has a quota of 1,200 queries per minute (QPM) for createexternaltransaction and refundexternaltransaction calls. The getexternaltransaction call is excluded from this limit. If you are migrating a large number of existing subscriptions, spread the work across multiple days or request a quota increase from Google.

Tax Handling for India

When reporting transactions for users in India, include the administrative area (state or province) because tax rates vary:

json
{
  "userTaxAddress": {
    "regionCode": "IN",
    "administrativeArea": "KARNATAKA"
  }
}

Payment Method Image Asset Requirements

If you use user choice billing, Google displays your alternative billing option on the choice screen alongside Google Play's option. Your listing shows your app icon, app name, and an image of your accepted payment methods. Google has specific requirements for this image.

Specifications

Property

Requirement

Asset size

192dp x 20dp

Format

PNG with transparent background

Card size

32dp x 20dp per card

Card spacing

8dp between cards

Inner padding

3dp within each card

Card outline

1dp inner stroke, 2dp corner radius, color #E0E0E0

Card background

Solid, preferably white

Maximum cards

5 payment method icons

Guidelines

  • Include only payment method logos in the image (Visa, Mastercard, PayPal, etc.)
  • Do not add text, promotional messaging, or other graphics
  • Use recognizable, standard logos for each payment method
  • Upload the asset through the Play Console under your alternative billing configuration
  • Images that do not meet these requirements will not be displayed on the choice screen

The payment method image is important for user trust. Users see this alongside Google Play's recognizable brand, so make sure your accepted methods are clearly visible and professional.

Want a simpler approach?

The RevenueCat SDK Handbook covers the same topics — with less code and a managed backend.

Related chapters

  • Chapter 6: The Purchase Flow

    End-to-end from launchBillingFlow() to acknowledgement: payment dialog, callbacks, pending purchases, and multi-quantity.

    Learn more
  • Chapter 1: Understanding Google Play's Billing System

    The big picture: key actors, core vocabulary, product types, and the three pillars of Google Play Billing.

    Learn more
  • Chapter 9: Backend Architecture for Billing

    The 7-step server verification flow: receive token, call the API, check signatures, grant entitlements, acknowledge.

    Learn more