| name | alternative-billing |
| description | Use this skill when implementing Google Play's alternative billing programs or external offers (introduced across PBL 8.2 to 8.3, current as of PBL 9.x). Covers user choice billing, alternative billing only, external transaction reporting, and the regional program eligibility. |
| license | Apache-2.0; see LICENSE |
| metadata | {"author":"RevenueCat","source":"google-play-handbook chapter 18","keywords":["android","play-billing","alternative-billing","user-choice-billing","external-offers","pbl9"]} |
Alternative Billing and External Offers
Google Play exposes several programs that let you present payment paths outside Google Play billing. This skill walks you through picking the right program for your region, wiring the PBL APIs, and reporting transactions to the externaltransactions API within 24 hours.
Phase 1: Discovery
Confirm program eligibility before you touch any code. Eligibility is driven by region, product type, and Play Console enrollment.
Check enrollment
- Open Play Console and confirm you accepted the program terms for each program you plan to use.
- Register external URLs, subscription management links, and countries per program.
- Record the PBL version in your
build.gradle. Features map to PBL versions:
| Program | Minimum PBL |
|---|
| User choice billing | 5.2 (6.1 renamed APIs) |
| Alternative billing only | 6.1 |
| External offers (unified API) | 8.2.1 |
| External content links | 8.2.1 |
| External payment links | 8.3.0 |
Map regions to programs
| Region | Available programs |
|---|
| South Korea | User choice billing |
| EEA | User choice billing, alternative billing only, external offers, external content links, external payment links |
| India | User choice billing |
| United States | User choice billing, external content links, external payment links |
Check runtime availability
Do not hard code country lists. The PBL surfaces eligibility.
billingClient.isBillingProgramAvailableAsync(
BillingProgram.EXTERNAL_OFFER,
listener
)
For alternative billing only, use isAlternativeBillingOnlyAvailableAsync(). A BILLING_UNAVAILABLE result means the user or account is ineligible.
Discovery questions to answer
- Which program(s) does your app need per region?
- Do you serve subscriptions, one time products, or both?
- Is PBL at a version that supports the target program?
- Does your backend have a job that can report transactions within 24 hours?
Phase 2: Plan
Pick the program variant that matches your goals and region coverage.
Program comparison
| Program | Flow | BillingClient setup | Token source |
|---|
| User choice billing | Google renders a choice screen | enableUserChoiceBilling(listener) | UserChoiceDetails.externalTransactionToken |
| Alternative billing only | No Google Play billing at all, info dialog required | enableAlternativeBillingOnly() | createAlternativeBillingOnlyReportingDetailsAsync() |
| External offers | Link out to a website for deals | enableBillingProgram(EXTERNAL_OFFER) | createBillingProgramReportingDetailsAsync() |
| External content links | Link out for digital content (US) | enableBillingProgram(EXTERNAL_CONTENT_LINK) | createBillingProgramReportingDetailsAsync() |
| External payment links | Choice screen with developer option | enableBillingProgram(EXTERNAL_PAYMENTS, devListener) | createBillingProgramReportingDetailsAsync() |
Token versus ID
externalTransactionToken: generated by the PBL, tied to one user and device context, attached to the initial report only. Generate a new token per transaction.
externalTransactionId: string you generate, uniquely identifies the transaction in your system. Each renewal uses a new ID referencing the first via initialExternalTransactionId. Do not put PII in the ID.
Backend plan
Before you write client code, plan these backend pieces:
- Endpoint that receives the token from the app and starts payment in your processor.
- Worker that calls
POST /androidpublisher/v3/applications/{packageName}/externalTransactions within 24 hours of charge.
- Renewal reporter that reuses
initialExternalTransactionId without the token.
- Refund reporter that calls
:refund on the original transaction ID.
- Quota planning:
createexternaltransaction and refundexternaltransaction share 1,200 QPM. Stagger migrations.
User choice billing flow plan
- User taps buy.
- You call
launchBillingFlow().
- Google renders the choice screen for eligible users.
- Google Play path: standard
PurchasesUpdatedListener fires.
- Your billing path:
UserChoiceBillingListener fires with token and products.
- You run your payment flow, then report through
externaltransactions.
Alternative billing only flow plan
- Call
isAlternativeBillingOnlyAvailableAsync().
- Call
showAlternativeBillingOnlyInformationDialog(). On USER_CANCELED, retry next attempt.
- Call
createAlternativeBillingOnlyReportingDetailsAsync() to get a fresh token.
- Send token plus product data to your backend.
- Complete payment in your processor.
- Report the transaction within 24 hours.
Phase 3: Execute
User choice billing client
val userChoiceListener = UserChoiceBillingListener { details ->
backend.startExternalPurchase(
details.externalTransactionToken,
details.products
)
}
val billingClient = BillingClient.newBuilder(context)
.setListener(purchasesUpdatedListener)
.enablePendingPurchases(
PendingPurchasesParams.newBuilder().build()
)
.enableUserChoiceBilling(userChoiceListener)
.build()
For subscription upgrades that stay in your billing system, set setOriginalExternalTransactionId() on SubscriptionUpdateParams to skip the choice screen.
Alternative billing only token
billingClient.createAlternativeBillingOnlyReportingDetailsAsync(
object : AlternativeBillingOnlyReportingDetailsListener {
override fun onAlternativeBillingOnlyTokenResponse(
result: BillingResult,
details: AlternativeBillingOnlyReportingDetails?
) {
if (result.responseCode != BillingResponseCode.OK) return
backend.send(details?.externalTransactionToken)
}
}
)
External offers or content links (PBL 8.2.1+)
val params = BillingProgramReportingDetailsParams.newBuilder()
.setBillingProgram(BillingProgram.EXTERNAL_OFFER)
.build()
billingClient.createBillingProgramReportingDetailsAsync(
params,
reportingListener
)
Launch the link after you persist the token:
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, listener)
External payment links (PBL 8.3+)
val devBillingListener = DeveloperProvidedBillingListener { details ->
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()
Attach a DeveloperBillingOptionParams to your BillingFlowParams when you call launchBillingFlow(). The choice screen renders only when the user is in a supported country, the program is enabled on the client, and you passed a developer option.
PBL 9 change: DeveloperProvidedBillingDetails.getLinkUri() is now @Nullable. The external payment link may not be resolved at selection time, so guard against both null and empty string before parsing it as a Uri:
val linkUri = details.linkUri
if (!linkUri.isNullOrEmpty()) {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(linkUri)))
} else {
showExternalPaymentUnavailableState()
}
Report the initial transaction
POST /androidpublisher/v3/applications/{packageName}/externalTransactions?externalTransactionId=txn-1
{
"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" }
}
Amounts use priceMicros. Multiply by 1,000,000. For India, include administrativeArea in userTaxAddress because tax rates vary by state.
Report renewals
Drop the token, add initialExternalTransactionId:
{
"recurringTransaction": {
"initialExternalTransactionId": "txn-1",
"externalSubscription": { "subscriptionType": "RECURRING" }
}
}
Report refunds
POST /androidpublisher/v3/applications/{packageName}/externalTransactions/{externalTransactionId}:refund
Supply the pre tax amount for partial refunds.
Verification checklist
- Report every transaction within 24 hours of the charge.
- Generate a new token per transaction. Never cache or reuse.
- Confirm PBL 8.2.1 or later if you use
createBillingProgramReportingDetailsAsync(). 8.2.0 has a bug.
- Strip PII from
externalTransactionId and from any external link Uri.
- Payment method image meets the 192dp x 20dp spec before you upload to Play Console.
- Remove any internal state before launching an external link. Users may not return to your app.
References