Android in-app purchases: Google Play Billing tutorial 2026

Vlad Guriev
Vlad Guriev
17 min read
Android in-app purchases: Google Play Billing tutorial 2026

TL;DR:

  • Billing Library 8 is mandatory for new submissions and updates. Current stable is 8.3.0.
  • Every subscription is built from three nested pieces: the subscription itself, base plans that set the billing period and price, and offers that add trials or introductory pricing.
  • queryProductDetailsAsync() returns one entry per base plan and offer combination, each with its own offer token. Pass that token to BillingFlowParams, or the purchase flow breaks.
  • Before you write billing code, sign the Paid Applications Agreement, link a merchant account, and upload a signed .aab to a test track. Play Console blocks product creation without all three.
  • SkuDetails, querySkuDetailsAsync(), and queryPurchaseHistoryAsync() are gone in BL8. The migration cheat sheet at the end maps every rename.
  • enableAutoServiceReconnection() replaces the manual retry logic in onBillingServiceDisconnected(), and enablePendingPurchases() now throws without a PendingPurchasesParams argument.
  • Google Play charges 15% on the first $1M of annual revenue and on subscriptions past the user's first 12 months, 30% above that.

Android in-app purchases let you sell digital products and subscriptions inside your app through Google Play's billing system. You define the product in Play Console, then connect to it from your app with the Google Play Billing Library. This guide walks through both halves of the current library version, and it is part 1 of a five-part series.

Two dates shape every Android billing project running right now. August 31, 2026, is the publishing gate: after that date, Google Play rejects new apps and app updates built against Billing Library 7 or older. June 30, 2026, is when Google changed how it charges you, splitting the old single commission into a service fee and a billing fee in the US, UK, and EEA.

In this article, you will learn how to:

  • Create a subscription in Google Play Console using the current model of subscriptions, base plans, and offers
  • Configure durations, prices, free trials, and introductory offers
  • Set up your Android project and query products with Billing Library 9

What is an in-app purchase?

In-app purchases, and especially auto-renewing subscriptions, remain the most reliable way to monetize an Android app. Subscriptions let you reinvest in content and product development, and they let users get a higher-quality app for a predictable recurring fee. Google Play in-app purchases fall into these categories:

  • Subscriptions:
    • Auto-renewable subscriptions. Google renews these automatically through recurring payments until the user cancels.
    • Prepaid plans. Users pay upfront for a fixed period with no auto-renewal. Google introduced these alongside the new subscription model in Billing Library 5. They are technically subscriptions but behave more like one-time access passes.
    • Non-renewing subscriptions. You implement these with one-time products that grant time-limited access, and you manage expiration yourself.
  • Consumable one-time products. Users buy and consume these repeatedly: in-game currency, power-ups, and lives.
  • Non-consumable one-time products. Users buy these once for permanent access: cosmetic items, lifetime unlock, and premium features.

ℹ️Starting with Billing Library 8.0, Google renamed "in-app items" to "one-time products." This article uses the new term throughout. For a broader definition of the business model, see our glossary entry on in-app subscriptions - https://adapty.io/glossary/in-app-subscriptions/.

This series focuses on subscriptions, since that is where the bulk of mobile app revenue sits in 2026.

Android in-app purchase implementation roadmap

The full integration runs across five articles. You are reading part 1:

  1. Android in-app purchases, part 1: configuration and adding to the project (this article)

Five articles are a fair indicator of how much there is to handle. If you would rather not write all this plumbing yourself, and especially if you want server-side validation, paywall A/B testing, and revenue analytics out of the box, see our companion guide on how to add Android in-app purchases with Adapty in 10 minutes.

What's new in Google Play Billing Library 8 and 9

Billing Library 9.0.0 shipped on May 19, 2026, and is the current major version. Billing Library 8.0.0 shipped a year earlier, on June 30, 2025.

The date that matters for your release schedule is August 31, 2026. After that, Google Play blocks new apps and updates to existing apps unless they are built against version 8 or later. Apps already on the Play Store keep transacting, so this is a publishing gate rather than a runtime switch. If you need more time, request an extension in Play Console for a deadline of November 1, 2026.

There is no shortcut from 7 straight to 9. Do the version 8 work first, then apply the four version 9 changes on top.

What version 9 changed

  • In-app messaging for opt-in price increases. Users confirm an upcoming price increase without leaving your app. Google shows the message from the first day the user can accept, and repeats it at most once every 7 days.
  • Blocked Play Store now returns BILLING_UNAVAILABLE. When the system blocks the Play Store app, for example, in an OEM-customized kids mode, the response code changed from ERROR to BILLING_UNAVAILABLE and the BillingResult carries a "Play Store is blocked" debug message. This needs AndroidX core 1.9 or later.
  • DeveloperProvidedBillingDetails.getLinkUri() is now @Nullable. Handle both null and an empty string before you parse the value or launch a browser intent.
  • targetSdkVersion moved to 35.

What version 8 changed, and what will break your build coming from 4, 5, 6, or 7

  • SkuDetails and querySkuDetailsAsync() are removed. They've been replaced by ProductDetails and queryProductDetailsAsync() since BL 5, and BL 8 fully drops the old APIs.
  • queryPurchaseHistoryAsync() is removed. Use queryPurchasesAsync() for active purchases, or rely on backend purchase history.
  • The onProductDetailsResponse() signature changed. The callback now returns an QueryProductDetailsResult object that contains both successfully fetched products and a list of unfetched products with status codes.
  • One-time products now support multiple purchase options and offers — the same flexibility that subscriptions got in BL 5.
  • Automatic service reconnection via enableAutoServiceReconnection(). The library now handles dropped connections internally, which significantly cuts down on SERVICE_DISCONNECTED errors.
  • Sub-response codes for launchBillingFlow() — for example, PAYMENT_DECLINED_DUE_TO_INSUFFICIENT_FUNDS — so you can show targeted error messages to the user.
  • Suspended subscriptions (BL 8.1+) — a new isSuspended() flag on Purchase for paused subscriptions and declined renewals.
  • External offers and external content links APIs (BL 8.2+) — for the alternative billing programs in regions where regulators (EU, US, South Korea) require them.

For the bigger picture on how the subscription model reached its current shape, see our deeper dive on the Google Play Billing Library 5 reworked subscription model.

Subscriptions, base plans, and offers: the 2026 model

If you remember one thing from this article, remember this section. Developers migrating from Billing Library 4 get stuck here because the Play Console UI no longer matches the old "one subscription equals one price plus one trial" mental model.

Since Billing Library 5, three nested entities make up every subscription:

EntityWhat it definesIdentifier
SubscriptionThe product itself: ID, name, description, benefits, tax categoryProduct ID (e.g., premium_access)
Base planBilling period, renewal type (auto-renew or prepaid), grace period, and regional pricingBase plan ID (e.g., monthlyyearly)
OfferFree trial, introductory price, eligibility rules; sits on top of a base planOffer ID + offer token (returned at runtime)

A practical example: instead of creating three separate subscription products for weekly, monthly, and yearly access (the old way), you now create one subscription called premium_access with three base plans: weeklymonthly, and yearly. You can then attach offers to any base plan — say, a 7-day free trial on the yearly plan and an introductory 50% off on the monthly plan for first-time buyers.

This matters at integration time because queryProductDetailsAsync() it returns an ProductDetails object whose subscriptionOfferDetails() is a list — one entry per (base plan + offer) combination available to the current user. Each entry has its own offer token, and you must pass that token BillingFlowParams when launching the purchase flow. Skipping the offer token, or passing the wrong one, is the single most common BL 8 integration bug.

Google Play fees and the 2026 regulatory landscape

Google changed its fee structure on June 30, 2026, in the United States, the United Kingdom, and the European Economic Area, splitting the old single commission into a service fee and a separate billing fee. Auto-renewing subscriptions carry a 10% service fee at every revenue tier, plus 5% when you use Google Play's billing system. Route payments through alternative billing or an external web link, and the 5% goes away, though your own processor takes its cut instead.

Whether that trade is worth making depends on your price point and plan length, and it is a business decision rather than an integration one. We covered the full rate card, the processor math, and what the change means for Android as a market, and what Google Play's new billing rules mean for subscriptions.

Billing choice, user choice billing, and the APIs behind them

Google's billing choice program went live on June 30, 2026, for developers serving users in the United Kingdom and the European Economic Area, alongside the existing programs in the United States. Through it, you can offer an alternative billing system or link users to your own website to complete a purchase, alongside Google Play's billing. You can also design your own choice screen instead of Google's default, as long as you follow Google's UX guidelines.

This program grew out of what Google originally called user choice billing, the pilot that let a user pick between Google Play's billing system and the developer's own at checkout. The APIs still carry the older naming: BillingClient.Builder.enableUserChoiceBilling() with UserChoiceBillingListener and UserChoiceDetails. Expect to see both names in Google's documentation for a while.

For the other programs, Billing Library 8.2.0 was added enableBillingProgram(), isBillingProgramAvailableAsync(), createBillingProgramReportingDetailsAsync(), and launchExternalLink(), covering external offers and external content links. Version 8.3.0 extended this to external payments through BillingProgram.EXTERNAL_PAYMENTS and BillingFlowParams.Builder.enableDeveloperBillingOption().

One compliance date to diary. If you are enrolled in the US external content links or alternative billing programs, Google's July 22, 2026, notice requires you to report transactions and successful downloads, and pay the relevant service fees, starting October 1, 2026. That reporting runs through createBillingProgramReportingDetailsAsync(), so it is your integration work, not just your finance team's.

None of this changes the basics of integrating Google Play Billing for the standard case. But if you serve users in the EU, US, UK, or South Korea and you are building from scratch in 2026, plan for alternative-billing UX from the start.

Setting up your Google Play developer account

Before you write a line of billing code, make sure you have:

  1. An active Google Play Console developer account with the one-time registration fee paid.
  2. All Play Console agreements signed, including the Developer Distribution Agreement and the Paid Applications Agreement. You need the second one to sell anything.
  3. A merchant account linked to your Play Console. Without it, the "Create product" buttons in the Monetize section stay greyed out.
  4. At least one signed App Bundle (.aab) uploaded to a test track. Google Play will not let you create products until it sees the package name in a real upload. You also need to declare the billing permission in your app's AndroidManifest.xml:
<uses-permission android:name="com.android.vending.BILLING" />
XML

Without this permission, Play Console blocks product creation on the build, and BillingClient.startConnection() fails at runtime.

Configuring a subscription in Google Play Console

Open Play Console, select your app, and in the left sidebar, go to Monetize → Products → Subscriptions. Click Create subscription.

The form has three logical groups: subscription metadata, base plan, and offers. Walk through them in that order.

Step 1. Subscription metadata
  • Product ID. The string your app passes to queryProductDetailsAsync(). Make it descriptive and stable, because you cannot change it after the subscription goes live. Use something like premium_access or pro_features, and keep duration out of the product ID. Duration belongs in the base plan ID.
  • Name. Users see this in the Google Play purchase UI and on the manage-subscriptions screen.
  • Description. Users see this too. Keep it short and concrete.
  • Benefits. Up to four short bullets covering what the subscription unlocks. Google surfaces these in the Play purchase sheet.
  • Tax category. Pick the closest match to your content type. This drives the EU VAT and US sales tax rates Google charges on your behalf.
Step 2. Add base plans

After you save the subscription, click Add base plan. For each one, configure:

  • Base plan ID. Lowercase, no spaces: monthly, annual, weekly.
  • Renewal type. Auto-renewing for traditional subscriptions, prepaid for time-limited access without auto-renew.
  • Billing period. Weekly, monthly, every 3 months, every 6 months, or yearly.
  • Grace period. When a renewal payment fails, Google keeps retrying for the grace period while you keep granting access: up to 30 days for monthly and longer plans, up to 7 days for weekly. Turn it on.
  • Account hold. When the grace period expires, the subscription enters account hold for up to 30 days. The user loses access but can still recover by fixing their payment method.
  • Pause. Let users pause monthly, 3-month, and 6-month subscriptions instead of canceling. This cuts churn.
  • Resubscribe. Let users resubscribe from the Play Store after cancellation, not just from inside your app.
  • Pricing. Set the price in your default currency. Play Console converts and applies VAT and sales tax per country, and you can override per-country prices manually. App Store Connect does not show tax breakdowns at this stage. Play Console does, which is a small quality-of-life win.
Step 3. Add offers (optional)

Offers sit on top of base plans and exist for acquisition and retention. Click Add offer on any base plan to configure:

  • Offer ID and name.
  • Eligibility criteria. The two common ones are "users who have never bought this subscription" for acquisition, and "developer-determined eligibility" so you can grant offers from your backend.
  • Offer phases. Up to two phases per offer: a free trial, then an optional introductory price, then the base plan price. For example, 7 days free, then 30 days at $1.99, then $9.99 per month. For how trial duration affects revenue, see our breakdown of free trial conversion rates for apps in 2026. Trials of 5 to 9 days hit the median 45% conversion sweet spot.

Once you save and activate the subscription, base plans, and offers, the Billing Library can fetch them. Activation is the step most newcomers miss.

Setting up your Android project for Billing Library 9

Add the Play Billing Library to your module-level build.gradle or build.gradle.kts:

dependencies {
    def billing_version = "9.0.0"
    implementation "com.android.billingclient:billing:$billing_version"
}
Groovy

If you use Kotlin, and you should, use the KTX module for coroutine-friendly extensions:

dependencies {
    def billing_version = "9.0.0"
    implementation "com.android.billingclient:billing-ktx:$billing_version"
}
Groovy

Billing Library 9 targets SDK 35, and the library minSdkVersion has been 23 since 8.1.0. If you are staying on version 8 until closer to the August 31, 2026 deadline, 8.3.0 is the last release in that line.

Initializing BillingClient with Billing Library 9

The pattern below is the modern equivalent of BillingClientWrapper this article. It is simpler now because enableAutoServiceReconnection() it handles dropped connections, and you no longer write retry logic in onBillingServiceDisconnected().

import android.content.Context
import com.android.billingclient.api.*
 
class BillingClientWrapper(context: Context) : PurchasesUpdatedListener {
 
    private val billingClient = BillingClient.newBuilder(context)
        .setListener(this)
        .enablePendingPurchases(
            PendingPurchasesParams.newBuilder()
                .enableOneTimeProducts()
                .enablePrepaidPlans()
                .build()
        )
        .enableAutoServiceReconnection()
        .build()
 
    fun startConnection(onReady: () -> Unit) {
        billingClient.startConnection(object : BillingClientStateListener {
            override fun onBillingSetupFinished(billingResult: BillingResult) {
                if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                    onReady()
                }
            }
 
            override fun onBillingServiceDisconnected() {
                // No-op: enableAutoServiceReconnection() handles reconnects
                // for us. Use this only for state logging if needed.
            }
        })
    }
 
    override fun onPurchasesUpdated(
        billingResult: BillingResult,
        purchases: MutableList<Purchase>?
    ) {
        // Purchase callbacks are handled in part 2 of this series.
    }
}
Kotlin

Two things worth flagging.

Google still recommends one BillingClient instance per app. Multiple connections produce duplicated PurchasesUpdatedListener callbacks and double-grant bugs. Hold the wrapper as a singleton through Hilt, Koin, or whatever DI framework you use.

enablePendingPurchases(PendingPurchasesParams) is required. Version 8 removed the parameterless overload, so calling enablePendingPurchases() with no arguments now fails.

Querying products with queryProductDetailsAsync()

To fetch products, you need their product IDs and types. Unlike the old SkuDetails API, you no longer make two separate calls for subscriptions and one-time products. QueryProductDetailsParams takes a mixed list:

interface OnQueryProductsListener {
    fun onSuccess(products: List<ProductDetails>)
    fun onFailure(error: Error)
}
 
class Error(val responseCode: Int, val debugMessage: String)
 
fun queryProducts(listener: OnQueryProductsListener) {
    val productList = listOf(
        QueryProductDetailsParams.Product.newBuilder()
            .setProductId("premium_access")
            .setProductType(BillingClient.ProductType.SUBS)
            .build(),
        QueryProductDetailsParams.Product.newBuilder()
            .setProductId("coin_pack_large")
            .setProductType(BillingClient.ProductType.INAPP)
            .build()
    )
 
    val params = QueryProductDetailsParams.newBuilder()
        .setProductList(productList)
        .build()
 
    billingClient.queryProductDetailsAsync(params) { result ->
        if (result.billingResult.responseCode ==
            BillingClient.BillingResponseCode.OK) {
 
            val products = result.productDetailsList
            val unfetched = result.unfetchedProductList
 
            // Log or surface unfetched products to the user
            unfetched.forEach { unfetchedProduct ->
                // unfetchedProduct.statusCode tells you why it failed
            }
 
            listener.onSuccess(products)
        } else {
            listener.onFailure(
                Error(
                    result.billingResult.responseCode,
                    result.billingResult.debugMessage
                )
            )
        }
    }
}
Kotlin

The callback receives aQueryProductDetailsResult, added in version 8. It exposes the successfully fetched products through productDetailsList a list of UnfetchedProduct objects for the ones that failed, each with a status code explaining why: product not found, no offers available to the user, and so on. Version 7 silently dropped unfetched products, which made debugging harder.

Each ProductDetails object exposes a localized name, description, product type, and one of two pricing payloads:

  • oneTimePurchaseOfferDetails for one-time products, holding the formatted price, price in micros, and currency code.
  • subscriptionOfferDetails for subscriptions. This is a list covering each base plan and each user-eligible offer combination, with billing period, pricing phases, and an offer token. You use those offer tokens to launch the purchase flow, which we cover in part 2 of this series.

Subscription lifecycle states

A subscription does not just exist in two states. Google Play tracks seven, and your app needs to grant or deny access correctly in each one:

StateDescriptionGrant access?
ActiveUser is subscribed and current on paymentsYes
CancelledUser cancelled, but the paid period has not endedYes, until expiration
In the grace periodRenewal payment failed, and Google is retryingYes
On holdGrace period expired, and Google is still retryingNo
PausedUser explicitly paused, where supportedNo
SuspendedRenewal method declined, isSuspended() returns trueNo
ExpiredSubscription ended, and the user churnedNo

The suspended state arrived in Billing Library 8.1.0 and replaces parts of the old paused and declined-payment handling. When you hit it, deny access and deep-link the user to the Play subscription center to update their payment method.

Migration cheat sheet: Billing Library 4 to 9

Updating an existing integration? Here is the minimum set of API renames to apply.

Billing Library 4 to 6 (deprecated or removed)Billing Library 8 (current)
SkuDetailsProductDetails
SkuDetailsParamsQueryProductDetailsParams
BillingClient.SkuType.SUBS / .INAPPBillingClient.ProductType.SUBS / .INAPP
querySkuDetailsAsync()queryProductDetailsAsync()
queryPurchaseHistoryAsync()Removed, use queryPurchasesAsync() plus backend
BillingFlowParams.setSkuDetails()setProductDetailsParamsList() Plus offer token
enablePendingPurchases() (no args)enablePendingPurchases(PendingPurchasesParams)
Manual reconnect in onBillingServiceDisconnected()enableAutoServiceReconnection()
Purchase.getSku()Purchase.getProducts(), returns a list
queryPurchasesAsync(String skuType, ...)queryPurchasesAsync(QueryPurchasesParams, ...)

Then version 8 to 9:

What to checkChange in version 9
Blocked Play Store handlingResponse code moved from ERROR to BILLING_UNAVAILABLE, needs AndroidX core 1.9+
DeveloperProvidedBillingDetails.getLinkUri()Now @Nullable, handle null and ""
Target SDKNow 35
Price increasesAdopt in-app messaging for opt-in price increases
Subscription replacementSubscriptionUpdateParams.setSubscriptionReplacementMode is deprecated, use SubscriptionProductReplacementParams.setReplacementMode

Google maintains an official migration guide for version 9 in the Android Developers documentation, and it walks through every cumulative removal on one page.

Comparing the configuration flow with App Store Connect

If you have shipped on iOS, the Play Console subscription configurator will feel familiar but better organized. Play Console shows tax breakdowns per country at the pricing step, and App Store Connect does not, and moving between subscriptions, base plans, and offers is faster than navigating App Store Connect's subscription group pages. Where iOS keeps the edge is monetization itself, since iOS subscriptions still earn more per user on average across most categories.

For the iOS counterpart of this guide, see our iOS in-app purchases tutorial.

Next steps

That covers configuration and BillingClient setup. Part 2 walks through the purchase flow itself: launching launchBillingFlow() with offer tokens, handling the onPurchasesUpdated() callback, acknowledging purchases, and the paywall UI patterns Google requires. Continue with part 2: processing purchases with the Google Play Billing Library.

If you build subscription apps and you would rather not write and maintain five articles' worth of native Billing Library code, book a free demo call. Adapty wraps Google Play Billing and the App Store equivalent into a single in-app purchases SDK with paywall A/B testing, server-side validation, and revenue analytics built in. We handle Billing Library upgrades so you do not have to.

FAQ

Related articles

Google Play server-side purchase validation
21 min read

Google Play server-side purchase validation

How to verify purchase for an Android app — receipt validation. Subscription acknowledgement, refund tracking, server notifications for transactions.

Kirill PotekhinKirill PotekhinRead

See how Adaptycan grow your app revenue