Implementing Google Play Billing (One-Time Purchases) for Android
Google Play Billing Library 6+ (mandatory for new applications since 2023, for updates since 2024) restructured the purchase API. BillingClient.queryPurchasesAsync replaced the synchronous queryPurchases, and PendingPurchasesParams was introduced for deferred transactions. Applications on BillingClient 4 and below are rejected during publication.
One-time products: INAPP vs DURABLE
In Play Billing 6+, one-time purchases are divided into two subtypes:
- INAPP (legacy type) — consumables and non-consumables in one cart
- DURABLE — explicitly non-consumable, new type with BillingClient 6
In practice, for "remove ads" and "unlock levels" we use ProductType.INAPP with acknowledged status as an ownership marker.
Acknowledgement — where everything breaks
Every purchase must be confirmed within 3 days through acknowledgePurchase() or consumePurchase() (for consumables). If not confirmed — Google automatically refunds the money and revokes the purchase. This is not obvious from the documentation.
val billingClient = BillingClient.newBuilder(context)
.setListener { billingResult, purchases ->
purchases?.forEach { purchase ->
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
if (!purchase.isAcknowledged) {
val params = AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
.build()
billingClient.acknowledgePurchase(params) { result ->
if (result.responseCode == BillingClient.BillingResponseCode.OK) {
unlockFeature(purchase.products.first())
}
}
}
}
}
}
.enablePendingPurchases(
PendingPurchasesParams.newBuilder()
.enableOneTimeProducts()
.build()
)
.build()
enablePendingPurchases() is now mandatory. Without it, BillingClient.startConnection() throws an exception. Pending purchases (cash payments through Google Pay partners in some regions) transition to PURCHASED not immediately — you need to listen for updates through PurchasesUpdatedListener.
Purchase restoration on reinstall
On app reinstall, purchases are restored through queryPurchasesAsync(QueryPurchasesParams). Call on every startup:
val params = QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.INAPP)
.build()
billingClient.queryPurchasesAsync(params) { billingResult, purchaseList ->
purchaseList.filter {
it.purchaseState == Purchase.PurchaseState.PURCHASED && it.isAcknowledged
}.forEach { restoreAccess(it) }
}
Server verification
For server-side applications — send purchaseToken to your backend, which verifies purchaseState and acknowledgementState through Google Play Developer API (purchases.products.get). Only then unlock content in the database.
Estimated time — 2–3 days: Billing Library 6 integration, handling all purchase states, server verification, testing through Google Play Console License Testing.







