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:
Or if you need a specific product by ID outside of Offerings:
The StoreProduct object returned by the SDK wraps the underlying ProductDetails from Google Play and exposes the same price and description fields:
Launching a Purchase
Pass a Package or StoreProduct to Purchases.purchase():
When you call awaitPurchase(), the SDK:
- Calls
BillingClient.launchBillingFlow()internally - Waits for the
PurchasesUpdatedListenerresult - Posts the purchase token to the RevenueCat backend for verification
- Acknowledges or consumes the purchase automatically
- Returns updated
CustomerInfowith 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:
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.
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.