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:
- Android in-app purchases, part 1: configuration and adding to the project (this article)
- Android in-app purchases, part 2: processing purchases with the Google Play Billing Library
- Android in-app purchases, part 3: retrieving active purchases and subscription change
- Android in-app purchases, part 4: error codes from the Billing Library and testing
- 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:
SkuDetailsandquerySkuDetailsAsync()are removed. They’ve been replaced byProductDetailsandqueryProductDetailsAsync()since BL 5, and BL 8 fully drops the old APIs.queryPurchaseHistoryAsync()is removed. UsequeryPurchasesAsync()for active purchases, or rely on backend purchase history.- The
onProductDetailsResponse()signature changed. The callback now returns aQueryProductDetailsResultobject 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 onSERVICE_DISCONNECTEDerrors. - 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 onPurchasefor 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:
| Entity | What it defines | Identifier |
|---|---|---|
| Subscription | The product itself: ID, name, description, benefits, tax category | Product ID (e.g., premium_access) |
| Base plan | Billing period, renewal type (auto-renew or prepaid), grace period, regional pricing | Base plan ID (e.g., monthly, yearly) |
| Offer | Free trial, introductory price, eligibility rules; sits on top of a base plan | Offer 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: weekly, monthly, 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:
| Scenario | Service fee | Notes |
|---|---|---|
| First $1M of annual revenue per developer | 15% | Applies automatically; no opt-in needed |
| Revenue above $1M per year | 30% | Standard rate for one-time products and first-year subs |
| Subscriptions after the user’s first 12 months | 15% | Reduced rate kicks in automatically |
| Media apps in the Play Media Experience Program | 10–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(), andlaunchExternalLink().
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:
- An active Google Play Console developer account with the one-time registration fee paid.
- All Play Console agreements signed, including the Developer Distribution Agreement and the Paid Applications Agreement (the latter is required to sell anything).
- A merchant account linked to your Play Console. Without it, the “Create product” buttons in the Monetize section will be greyed out.
- 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:
<uses-permission android:name="com.android.vending.BILLING" />XMLWithout 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 ispremium_access,pro_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.,
monthly,annual,weekly. - 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):
dependencies {
def billing_version = "8.3.0"
implementation "com.android.billingclient:billing:$billing_version"
}GroovyIf you’re using Kotlin (and you should be), use the KTX module for coroutine-friendly extensions:
dependencies {
def billing_version = "8.3.0"
implementation "com.android.billingclient:billing-ktx:$billing_version"
}GroovyBilling 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().
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.
}
}KotlinTwo things worth flagging:
- Google still recommends a single
BillingClientinstance per app. Multiple connections cause duplicatedPurchasesUpdatedListenercallbacks 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; callingenablePendingPurchases()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:
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
)
)
}
}
}KotlinThe 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:
| State | Description | Grant access? |
|---|---|---|
| Active | User is subscribed and current on payments | Yes |
| Cancelled | User cancelled but the paid period hasn’t ended | Yes (until expiration) |
| In grace period | Renewal payment failed; Google is retrying | Yes |
| On hold | Grace period expired; Google still retrying | No |
| Paused | User explicitly paused (where supported) | No |
| Suspended | Renewal method declined; isSuspended() is true | No |
| Expired | Subscription ended; user is churned | No |
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) |
|---|---|
SkuDetails | ProductDetails |
SkuDetailsParams | QueryProductDetailsParams |
BillingClient.SkuType.SUBS / .INAPP | BillingClient.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
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. 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. 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. 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. 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. 



