Implementing in-app purchases in you Android app

Last updated April 28, 2026 
by 
Vlad Guriev
Published January 27, 2021 
Last updated April 28, 2026
17 min read
612ce594ea2d50f58ccda3b0 Android Tutorial 1 Configuration

This is the first article in our updated series on implementing in-app purchases in Android apps. We’ve fully refreshed it for Google Play Billing Library 8, which became mandatory for all new app submissions and updates on August 31, 2025. If you’re still on Billing Library 4, 5, or 6, the code in this guide is how you should be writing it now.

In this part of the series, you’ll learn how to:

  • Create a product in Google Play Console using the modern subscription model (subscriptions, base plans, and offers)
  • Configure subscriptions: durations, prices, free trials, and introductory offers
  • Set up your Android project for Billing Library 8 and add a list of products to your app

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 developers 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 the following categories:

  • Subscriptions:
    • Auto-renewable subscriptions — renewed automatically through recurring payments until the user cancels.
    • Prepaid plans — added in Billing Library 6, these let users pay upfront for a fixed period without auto-renewal. They are technically subscriptions but behave more like one-time access passes.
    • Non-renewing subscriptions — implemented via one-time products that grant time-limited access; you manage expiration manually.
  • Consumable one-time products — purchased and consumed repeatedly (in-game currency, power-ups, lives).
  • Non-consumable one-time products — purchased once for permanent access (cosmetic items, lifetime unlock, premium features).

One terminology note: starting with Billing Library 8.0, Google officially renamed “in-app items” to “one-time products.” Throughout this article we’ll use the new terms. For a broader definition of the business model, see our glossary entry on in-app subscriptions.

This series focuses primarily on subscriptions, since that’s where the bulk of mobile app revenue lives in 2026.

Android in-app purchase implementation roadmap

The full integration is covered across five articles. You’re reading part 1 — make sure to check the rest:

  1. Android in-app purchases, part 1: configuration and adding to the project (this article)
  2. Android in-app purchases, part 2: processing purchases with the Google Play Billing Library
  3. Android in-app purchases, part 3: retrieving active purchases and subscription change
  4. Android in-app purchases, part 4: error codes from the Billing Library and testing
  5. Android in-app purchases, part 5: server-side purchase validation

Five articles is a fair indicator of how much there is to handle. If you’d rather not write all this plumbing yourself — and especially if you want server-side validation, A/B testing on paywalls, and revenue analytics out of the box — check 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

Billing Library 8.0 shipped in mid-2025 and became mandatory for new app submissions on August 31, 2025. As of this writing, the current stable release is 8.3.0. If you’re migrating from BL 4, 5, 6, or 7, here are the changes that will actually break your build:

  • 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 a 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 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 evolved into its current shape, see our deeper dive on the Google Play Billing Library 5 reworked subscription model and the Google I/O 2024 subscription updates.

Subscriptions, base plans, and offers: the 2026 model

If you only remember one thing from this article, remember this section. The biggest source of confusion when developers migrate from BL 4 is that the Play Console UI no longer matches the old “one subscription = one price + one trial” mental model.

Since Billing Library 5, every subscription is built from three nested entities:

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, 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() returns a 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 to 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

In-app purchases on Google Play are subject to a service fee, and the rate depends on the developer and the type of revenue:

ScenarioService feeNotes
First $1M of annual revenue per developer15%Applies automatically; no opt-in needed
Revenue above $1M per year30%Standard rate for one-time products and first-year subs
Subscriptions after the user’s first 12 months15%Reduced rate kicks in automatically
Media apps in the Play Media Experience Program10–15%Eligibility-based

Beyond standard pricing, 2026 brings a noticeably more complex regulatory environment that affects how you can bill users:

  • EU Digital Markets Act (DMA): developers in the European Economic Area can use third-party app stores, sideloading, and alternative billing systems with reduced or zero Google Play fees on EEA users.
  • Epic v. Google (United States): following the October 2025 injunction compliance, Google Play allows alternative payment methods in U.S. apps. Developers using alternative billing in the U.S. must enroll in Google’s program by the deadlines published on the Play Console policy page; fee structures for the program are still in flux pending settlement hearings.
  • External offers and external content links are supported via dedicated APIs in Billing Library 8.2+: enableBillingProgram()isBillingProgramAvailableAsync()createBillingProgramReportingDetailsAsync(), and launchExternalLink().

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

Setting up your Google Play developer account

Before you write a single 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 (the latter is required to sell anything).
  3. merchant account linked to your Play Console. Without it, the “Create product” buttons in the Monetize section will be greyed out.
  4. At least one signed App Bundle (.aab) uploaded to a test track of your app. Google Play won’t 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:

XML
<uses-permission android:name="com.android.vending.BILLING" />
XML

Without this permission, Play Console blocks you from creating in-app products on the build, and at runtime BillingClient.startConnection() will fail.

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

Step 1. Subscription metadata

  • Product ID. This is the string your app passes to queryProductDetailsAsync(). Make it descriptive and stable — you cannot change it after the subscription is activated. A sensible convention is premium_accesspro_features, or similar; keep duration out of the product ID and put it in the base plan ID instead.
  • Name. Shown to users in the Google Play purchase UI and on the manage-subscriptions screen.
  • Description. Also shown to users — keep it short and concrete.
  • Benefits. Up to four short bullet points highlighting what the subscription unlocks. Surfaced in the Play purchase sheet.
  • Tax category. Pick the closest match to your content type. This affects EU VAT and U.S. sales tax rates Google charges on your behalf.

Step 2. Add base plans

After saving the subscription, click Add base plan. For each base plan, configure:

  • Base plan ID. Lowercase, no spaces, e.g., monthlyannualweekly.
  • Renewal type. Choose Auto-renewing for traditional subscriptions or Prepaid for time-limited access without auto-renew.
  • Billing period. Weekly, monthly, every 3 months, every 6 months, or yearly.
  • Grace period. If a renewal payment fails, Google keeps retrying for the grace period (up to 30 days for monthly+, up to 7 days for weekly) while you continue granting access. Highly recommended.
  • Account hold. If grace period expires, the subscription enters account hold (up to 30 days) where the user loses access but can still recover with a fixed payment method.
  • Pause. Lets users pause monthly, every-3-month, and every-6-month subscriptions instead of canceling. Reduces churn significantly.
  • Resubscribe. Allows users to resubscribe from the Play Store (not just from inside your app) after cancellation.
  • Pricing. Set the price in your default currency. Play Console converts and applies VAT/sales tax automatically per country, but you can override per-country prices manually. App Store Connect doesn’t show tax breakdowns at this stage — Play Console does, and it’s a small but real quality-of-life win for developers.

Step 3. Add offers (optional)

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

  • Offer ID and name.
  • Eligibility criteria. Most common: “users who have never bought this subscription” (acquisition) or “developer-determined eligibility” (lets you grant offers via your backend).
  • Offer phases. Up to two phases per offer: free trial, then optional introductory price, then the base plan price. For example: 7 days free → 30 days at $1.99 → standard $9.99/month.

For a deeper look at how trial duration affects revenue, see our breakdown of free trial conversion rates for apps in 2026 — the punchline is that 5–9 day trials hit the median 45% conversion sweet spot.

Once you save and activate the subscription, base plans, and offers, they become fetchable via the Billing Library. Activation is the step most newcomers miss.

Setting up your Android project for Billing Library 8

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

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

If you’re using Kotlin (and you should be), use the KTX module for coroutine-friendly extensions:

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

Billing Library 8 requires compileSdk 34 or higher. There’s no separate minSdk bump beyond what your app already targets.

Initializing BillingClient with Billing Library 8

The pattern below is the modern equivalent of the old BillingClientWrapper we shipped in this article in 2021. It’s simpler now because enableAutoServiceReconnection() handles dropped connections automatically — you no longer need to write your own retry logic in onBillingServiceDisconnected().

Kotlin
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 a single BillingClient instance per app. Multiple connections cause duplicated PurchasesUpdatedListener callbacks and double-grant bugs. Hold the wrapper as a singleton (Hilt, Koin, or your DI framework of choice).
  • enablePendingPurchases(PendingPurchasesParams) is required. The old parameterless overload from BL 6 is deprecated; calling enablePendingPurchases() with no arguments now throws at runtime.

Querying products with queryProductDetailsAsync()

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

Kotlin
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 a QueryProductDetailsResult object — a Billing Library 8 addition. It exposes both the successfully fetched products (productDetailsList) and a list of UnfetchedProduct objects for products that couldn’t be fetched, each with a status code explaining why (product not found, no offers available to the user, and so on). This is a meaningful improvement over BL 7, which silently dropped unfetched products and made debugging harder.

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

  • oneTimePurchaseOfferDetails — for one-time products. Contains the formatted price, price in micros, and currency code.
  • subscriptionOfferDetails — for subscriptions. A list of offers (each base plan + each user-eligible offer combination), with billing period, pricing phases, and an offer token.

You’ll use those offer tokens to launch the purchase flow, which we cover in part 2 of this series.

Subscription lifecycle states

A subscription doesn’t just exist in two states (active vs. cancelled). Google Play tracks seven distinct states, 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 hasn’t endedYes (until expiration)
In grace periodRenewal payment failed; Google is retryingYes
On holdGrace period expired; Google still retryingNo
PausedUser explicitly paused (where supported)No
SuspendedRenewal method declined; isSuspended() is trueNo
ExpiredSubscription ended; user is churnedNo

The Suspended state is new in Billing Library 8.1 and replaces parts of the old paused/declined-payment handling. When you encounter it, don’t grant access and instead deep-link the user to the Play subscription center where they can update their payment method.

Migration cheat sheet: Billing Library 4 → 8

If you’re updating an existing integration, here’s the minimum set of API renames you need to apply:

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

For a complete migration walkthrough including code diffs, Google maintains an official “Migrate to Google Play Billing Library 8” guide in the Android Developers documentation.

Comparing the configuration flow with App Store Connect

If you’ve shipped on iOS before, the Play Console subscription configurator will feel familiar but better organized. Play Console shows tax breakdowns per country at the pricing step (App Store Connect doesn’t), and the navigation between subscriptions, base plans, and offers is genuinely faster than App Store Connect’s subscription group pages. Where iOS still has the edge: monetization itself — subscriptions on iOS 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 the BillingClient setup. In the next article, we walk through the actual purchase flow: 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’re an Android app developer or a product owner looking to escalate your conversion rates and in-app subscription revenue without writing — and maintaining — five articles’ worth of native Billing Library code, schedule a free demo call with us. 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 don’t have to.

FAQ

Billing Library 8 fully removes querySkuDetailsAsync()queryPurchaseHistoryAsync(), and the parameterless enablePendingPurchases(). It also adds automatic service reconnection, sub-response codes for launchBillingFlow(), multiple purchase options for one-time products, and a richer QueryProductDetailsResult that returns information about products that couldn’t be fetched. BL 7 still had backwards-compat shims for some of these; BL 8 doesn’t.

Yes. SkuDetails was deprecated in Billing Library 5 and removed in Billing Library 8. As of August 31, 2025, all new app submissions and updates to Google Play must target Billing Library 7 or higher. If your app still uses SkuDetails, you can request an extension through Play Console, but new releases won’t be accepted indefinitely.

Google Play automatically refunds the purchase and revokes the entitlement. Calling acknowledgePurchase() for subscriptions and non-consumable one-time products, or consumeAsync() for consumables, is mandatory within 72 hours of the purchase. We cover the implementation details in part 2 of this series.

Add testers to the License Testing list in Play Console (Settings → License testing), then have them join an internal, closed, or open test track of your app. Testers can complete the purchase flow with test cards (always-approves, always-declines, slow-test-card) without being charged real money. The app must be uploaded as a signed AAB to a track at least once before this works. We go deeper on testing in part 4 of the series.

The subscription is the product itself (ID, name, benefits). A base plan defines a billing period and price for that subscription — you can have multiple base plans (weekly, monthly, yearly) under one subscription. An offer is a discount or free trial that sits on top of a base plan, with its own eligibility rules. At runtime, queryProductDetailsAsync() returns one ProductDetails per subscription, with a list of (base plan + offer) combinations available to the current user. You pass the matching offer token to BillingFlowParams to launch the purchase.

Partially. The standard 30% rate still applies to one-time products and the first year of subscription revenue above $1M per developer. Subscriptions drop to 15% after the user’s first 12 months of continuous subscription. Developers earning under $1M per year pay 15% on all revenue automatically. Alternative billing programs in the EU, U.S., and South Korea may have different fee structures depending on the jurisdiction and whether you enroll in Google’s external offers program.

It depends on the region. In the EU, the Digital Markets Act allows third-party billing, sideloading, and alternative app stores for EEA users. In the U.S., following the Epic v. Google injunction, Google Play allows alternative payment methods, but developers using them in the U.S. must enroll in Google’s program. In most other regions, Google Play Billing is still the only allowed in-app purchase mechanism for digital goods. Always check the current Play Console policy page before architecting around alternative billing.

Billing Library 8 requires compileSdk 34 or higher. The minimum Android version your app actually runs on can be much lower (the library itself supports back to API 21 / Android 5.0), but new app submissions on Google Play must target API 35 (Android 15) as of August 31, 2025, regardless of which Billing Library version you use.
Vlad Guriev
Android developer and mobile app expert
Android
Tutorial

On this page

Ready to create your first paywall with Adapty?
Build money-making paywalls without coding
Get started for free