# Alternative Billing

> 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.

- Skill: `revenuecat/alternative-billing-2` (Agent Skill)
- Install (CLI): `npx skillmds@latest add revenuecat/alternative-billing-2`
- Raw SKILL.md: https://api.skillmd.com/api/skills/revenuecat/alternative-billing-2/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: Apache-2.0; see LICENSE
- Author: revenuecat (https://skillmd.com/u/revenuecat)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/revenuecat/alternative-billing-2

---


# 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

1. Open Play Console and confirm you accepted the program terms for each program you plan to use.
2. Register external URLs, subscription management links, and countries per program.
3. 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.

```kotlin
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:

1. Endpoint that receives the token from the app and starts payment in your processor.
2. Worker that calls `POST /androidpublisher/v3/applications/{packageName}/externalTransactions` within 24 hours of charge.
3. Renewal reporter that reuses `initialExternalTransactionId` without the token.
4. Refund reporter that calls `:refund` on the original transaction ID.
5. Quota planning: `createexternaltransaction` and `refundexternaltransaction` share 1,200 QPM. Stagger migrations.

### User choice billing flow plan

1. User taps buy.
2. You call `launchBillingFlow()`.
3. Google renders the choice screen for eligible users.
4. Google Play path: standard `PurchasesUpdatedListener` fires.
5. Your billing path: `UserChoiceBillingListener` fires with token and products.
6. You run your payment flow, then report through `externaltransactions`.

### Alternative billing only flow plan

1. Call `isAlternativeBillingOnlyAvailableAsync()`.
2. Call `showAlternativeBillingOnlyInformationDialog()`. On `USER_CANCELED`, retry next attempt.
3. Call `createAlternativeBillingOnlyReportingDetailsAsync()` to get a fresh token.
4. Send token plus product data to your backend.
5. Complete payment in your processor.
6. Report the transaction within 24 hours.

## Phase 3: Execute

### User choice billing client

```kotlin
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

```kotlin
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+)

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

billingClient.createBillingProgramReportingDetailsAsync(
    params,
    reportingListener
)
```

Launch the link after you persist the token:

```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, listener)
```

### External payment links (PBL 8.3+)

```kotlin
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`:
>
> ```kotlin
> val linkUri = details.linkUri
> if (!linkUri.isNullOrEmpty()) {
>     startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(linkUri)))
> } else {
>     showExternalPaymentUnavailableState()
> }
> ```

### Report the initial transaction

```http
POST /androidpublisher/v3/applications/{packageName}/externalTransactions?externalTransactionId=txn-1
```

```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" }
}
```

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`:

```json
{
  "recurringTransaction": {
    "initialExternalTransactionId": "txn-1",
    "externalSubscription": { "subscriptionType": "RECURRING" }
  }
}
```

### Report refunds

```http
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

- [Full chapter](https://www.revenuecat.com/guides/google-play-billing/alternative-billing-and-external-offers)

