Back to the RevenueCat homepage
RevenueCat SDKGoogle Play Billing

Chapter 3: One-Time Products with RevenueCat

Implementing one-time products from scratch means querying ProductDetails, building BillingFlowParams, handling the PurchasesUpdatedListener, acknowledging purchases, consuming consumables, managing pending states, and verifying receipts server side. Each of those steps requires code you own and maintain.

With RevenueCat, the purchase flow is three method calls: fetch offerings, launch purchase, check the result.

Querying Products

You do not call queryProductDetailsAsync(). You fetch Offerings:

kotlin
// Coroutine style
val offerings = Purchases.sharedInstance.awaitOfferings()
val product = offerings.current?.availablePackages?.first()?.product

Or if you need a specific product by ID outside of Offerings:

kotlin
val products = Purchases.sharedInstance.awaitGetProducts(
    listOf("your_product_id"),
    type = ProductType.INAPP
)
val product = products.firstOrNull()

The StoreProduct object returned by the SDK wraps the underlying ProductDetails from Google Play and exposes the same price and description fields:

kotlin
val price = product?.price?.formatted    // e.g. "$4.99"
val title = product?.title
val description = product?.description

Launching a Purchase

Pass a Package or StoreProduct to Purchases.purchase():

kotlin
// Using a Package from Offerings (recommended)
val packageToPurchase = offerings.current?.availablePackages?.first()
    ?: return

try {
    val result = Purchases.sharedInstance.awaitPurchase(
        PurchaseParams.Builder(activity, packageToPurchase).build()
    )
    // result.customerInfo has updated entitlements
    // result.storeTransaction has the purchase token
    grantAccess()
} catch (e: PurchasesTransactionException) {
    if (!e.userCancelled) {
        showError(e.error.message)
    }
}

When you call awaitPurchase(), the SDK:

  1. Calls BillingClient.launchBillingFlow() internally
  2. Waits for the PurchasesUpdatedListener result
  3. Posts the purchase token to the RevenueCat backend for verification
  4. Acknowledges or consumes the purchase automatically
  5. Returns updated CustomerInfo with the new entitlements

Steps 3, 4, and 5 happen automatically. You do not write any of that code.

Acknowledgement and Consumption

You do not call acknowledgePurchase() or consumeAsync() yourself. The SDK handles both:

  • Non-consumable products: acknowledged automatically after the RevenueCat backend confirms the purchase.
  • Consumable products: you need to tell RevenueCat the product is consumable. In the RevenueCat dashboard, mark the product as a consumable. After that, the SDK calls consumeAsync() automatically when the purchase is verified.

This means you no longer risk the 3-day acknowledgement window expiring because of a bug in your code.

Checking Purchase History

To check whether a user has purchased a non-consumable product:

kotlin
val customerInfo = Purchases.sharedInstance.awaitCustomerInfo()

// Via entitlements (recommended)
val hasPurchased = customerInfo.entitlements["lifetime_access"]?.isActive == true

// Via non-subscription transactions
val hasLifetime = customerInfo.nonSubscriptionTransactions
    .any { it.productIdentifier == "your_lifetime_product_id" }

The entitlement approach is preferred because it abstracts you from specific product IDs.

Handling Pending Purchases

RevenueCat does not grant entitlements for pending purchases. The awaitPurchase() call throws PurchasesTransactionException with code PaymentPendingError when a purchase enters the pending state. The exception's userCancelled property is false.

kotlin
} catch (e: PurchasesTransactionException) {
    if (e.error.code == PurchasesErrorCode.PaymentPendingError) {
        showPendingPaymentMessage()
    } else if (!e.userCancelled) {
        showError(e.error.message)
    }
}

RevenueCat will automatically detect when the pending payment completes (via RTDN processing on their backend) and update the entitlement at that point. Your app will receive the update through the UpdatedCustomerInfoListener.

Server-Side Verification

You do not implement server-side purchase verification. RevenueCat does it for you on every purchase. After awaitPurchase() returns successfully, the purchase has already been verified against the Google Play Developer API by RevenueCat's backend.

If you need to know the purchase happened on your own server (for example, to provision access in your database), subscribe to RevenueCat webhooks (Chapter 10). The INITIAL_PURCHASE event fires for every new non-subscription purchase.

Common Pitfalls

Calling queryProductDetailsAsync() directly. Once you integrate RevenueCat, you should not call BillingClient APIs directly. The SDK owns the BillingClient instance. Calling BillingClient methods alongside RevenueCat will produce undefined behavior.

Granting access before awaitPurchase() returns. The method only returns after the RevenueCat backend has verified the purchase. The returned CustomerInfo is the source of truth, use it, not optimistic local state.

Ignoring PaymentPendingError. Pending purchases are real purchases waiting to be completed. Show a message and listen for the UpdatedCustomerInfoListener to fire when the payment completes.

Prefer building from scratch?

The Google Play Billing Handbook covers the same topics with raw BillingClient, Developer API, and RTDNs.

Related chapters

  • Chapter 6: The Purchase Flow

    awaitPurchase() handles the complete billing flow, verification, and acknowledgement internally.

    Learn more
  • Chapter 5: Configuring the SDK

    A single configure() call replaces the entire connection lifecycle, reconnection, and sync logic.

    Learn more
  • Chapter 4: Subscriptions with RevenueCat

    Offerings → Packages → SubscriptionOptions. The complete product hierarchy, clearly simplified.

    Learn more