You've shipped a working Android app with Google Play Billing Library (GPBL) and raw BillingClient code. The app works—but maintaining it is exhausting. Every time Google releases a new GPBL version, something breaks. You're hand-rolling connection lifecycle management, retry logic after SERVICE_DISCONNECTED, receipt acknowledgement, purchase verification, server-side token validation, and subscription status checks. And that's before you've written a single line of product logic.
This guide shows you how to migrate that existing GPBL implementation to RevenueCat—without losing your current subscribers—so you can stop maintaining billing infrastructure and start shipping features.
Why Raw GPBL Is Painful to Maintain
Before diving into the migration, it's worth naming the specific pain points that RevenueCat eliminates:
1. BillingClient connection lifecycle is fragile. You must connect before every operation, handle SERVICE_DISCONNECTED, implement exponential backoff on SERVICE_UNAVAILABLE, and ensure you're not leaking connections across Activity lifecycles. One missed edge case and purchases silently fail.
2. Purchase acknowledgement is your problem. If you don't call acknowledgePurchase() within 3 days, Google automatically refunds the user. You need to track acknowledgement state yourself, usually in a database, and handle the case where your server went down between the purchase and the acknowledgement.
3. Server-side receipt validation is non-trivial. Verifying a purchaseToken against the Google Play Developer API requires OAuth2 credentials, token refresh handling, API rate limits, and cross-referencing against your own subscription state machine.
4. Subscription status is a snapshot, not a stream. The BillingClient.queryPurchasesAsync() call only shows currently owned items. Renewals, expirations, grace periods, and billing retries happen server-side—you have to poll or build a webhook pipeline to stay current.
5. GPBL major versions remove APIs. GPBL 8 (the version underpinning RevenueCat Android SDK v9) removed the ability to query expired subscriptions and consumed one-time products entirely. Every major version bump risks breaking your code.
RevenueCat wraps all of this in a backend and SDK that handles it for you. Source
Two Migration Paths
There are two ways to migrate, depending on how far along your own IAP code is and whether you want to rip-and-replace or run both systems in parallel. Source
Path A: Full SDK Migration (Recommended for New/Simple Apps)
Replace your BillingClient code entirely with the RevenueCat SDK. RevenueCat automatically acknowledges purchases, validates receipts server-side, and tracks subscription state. This is the cleanest approach if you don't have heavily customized purchase flows.
Path B: SDK + Existing IAP Code (Formerly "Observer Mode")
Keep your existing BillingClient purchase code intact and run RevenueCat alongside it. You set purchasesAreCompletedBy to MY_APP so RevenueCat doesn't try to complete purchases itself—it simply observes and records them. This is the right path if you have a complex custom purchase flow you're not ready to migrate. Source
Both paths can coexist with your existing subscribers—but you need a separate subscriber migration strategy (covered below).
Step 1: Create a RevenueCat Project and Connect Google Play
Before touching your app code:
- Sign up for a free RevenueCat account and create a project.
- Connect your Google Play app by uploading your package name and Google Service Credentials — ensure you grant Financial Access, which is required for receipt validation and historical imports.
- Enable Google Real-Time Developer Notifications so RevenueCat learns about renewals, expirations, and billing retries without polling. Source
Step 2: Configure Products, Entitlements, and Offerings
RevenueCat uses three concepts to manage what users buy and what they unlock: Source
| Concept | What it is | Example |
|---|---|---|
| Product | A purchasable item in Google Play | premium_monthly |
| Entitlement | The access level a product unlocks | premium |
| Offering | A group of products shown in your paywall | default |
The flow: User purchases a Product → unlocks an Entitlement → your app checks the entitlement to grant access.
In the RevenueCat dashboard:
- Navigate to Product catalog → Products and import your existing Google Play product IDs.
- Navigate to Product catalog → Entitlements, create an entitlement (e.g.,
"premium"), and attach your products to it. - Navigate to Product catalog → Offerings, create a default offering, and add packages that wrap your products.
The entitlement identifier ("premium") is what you'll check in code — not the raw product ID. This decouples your app logic from store-specific product IDs. Source
Step 3: Install the RevenueCat Android SDK
Add the dependency to your module-level build.gradle:
// build.gradle.kts (module)
dependencies {
implementation("com.revenuecat.purchases:purchases:9+")
}
Check the latest release on GitHub for the current version. Source
Important: Verify your Activity's
launchModeis set tostandardorsingleTopinAndroidManifest.xml. When a user's payment method requires external bank verification, they leave your app temporarily. AlaunchModeofsingleTaskorsingleInstancewill cause the purchase to be cancelled when they background your app. Source
<!-- AndroidManifest.xml -->
<activity
android:name=".MainActivity"
android:launchMode="singleTop" />
Step 4: Initialize the SDK
Configure Purchases once, early in your Application class. Never configure it in Activity.onCreate() — that creates multiple instances.
import android.app.Application
import com.revenuecat.purchases.LogLevel
import com.revenuecat.purchases.Purchases
import com.revenuecat.purchases.PurchasesConfiguration
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// Enable verbose logs during development
Purchases.logLevel = LogLevel.DEBUG
Purchases.configure(
PurchasesConfiguration.Builder(
context = this,
apiKey = "your_google_play_api_key" // From RevenueCat dashboard → API Keys
)
.appUserID("user_123") // Pass your own user ID, or null for anonymous
.build()
)
}
}
Your API key lives in Project Settings → API Keys → App specific keys in the RevenueCat dashboard. Use the Android-specific key (not a Test Store key) for production builds. Source
⚠️ Critical: Never ship a build with a Test Store API key to Google Play. Use build variants or environment variables to switch keys between debug and release builds. Source
Step 5A (Full Migration) — Replace BillingClient Purchase Code
Fetching Products
Replace your BillingClient.queryProductDetailsAsync() call with Purchases.sharedInstance.getOfferings():
// Before (raw GPBL)
val productList = listOf(
QueryProductDetailsParams.Product.newBuilder()
.setProductId("premium_monthly")
.setProductType(BillingClient.ProductType.SUBS)
.build()
)
billingClient.queryProductDetailsAsync(
QueryProductDetailsParams.newBuilder().setProductList(productList).build()
) { billingResult, productDetailsList ->
// handle result, check billingResult.responseCode, etc.
}
// After (RevenueCat)
Purchases.sharedInstance.getOfferingsWith(
onError = { error -> Log.e("RC", "Failed to fetch offerings: ${error.message}") },
onSuccess = { offerings ->
val monthly = offerings.current
?.availablePackages
?.firstOrNull { it.packageType == PackageType.MONTHLY }
// Bind monthly.storeProduct.price.formatted to your UI
}
)
Making a Purchase
Replace BillingClient.launchBillingFlow() with Purchases.sharedInstance.purchase():
// Before (raw GPBL) - you must handle acknowledgement, connection state, etc.
val billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(listOf(productDetailsParams))
.build()
billingClient.launchBillingFlow(activity, billingFlowParams)
// ... then in onPurchasesUpdated: validate, acknowledge, update your backend
// After (RevenueCat) - validation + acknowledgement handled automatically
Purchases.sharedInstance.purchaseWith(
purchaseParams = PurchaseParams.Builder(activity, packageToPurchase).build(),
onError = { error, userCancelled ->
if (!userCancelled) {
showError(error.message)
}
},
onSuccess = { storeTransaction, customerInfo ->
if (customerInfo.entitlements["premium"]?.isActive == true) {
unlockPremiumContent()
}
}
)
RevenueCat automatically acknowledges and validates the transaction server-side. Source
Checking Subscription Status
Replace your own subscription state machine with a single CustomerInfo check:
// Before: query your own database, cross-reference with Play billing state,
// handle grace periods and billing retries manually...
// After (RevenueCat)
Purchases.sharedInstance.getCustomerInfoWith(
onError = { error -> /* handle gracefully */ },
onSuccess = { customerInfo ->
val isPremium = customerInfo.entitlements["premium"]?.isActive == true
updateUI(isPremium)
}
)
CustomerInfo is cached locally, so this call is fast in most cases — safe to call on any screen that gates premium content. Source
You can also react to subscription changes in real time by implementing the UpdatedCustomerInfoListener:
Purchases.sharedInstance.updatedCustomerInfoListener = UpdatedCustomerInfoListener { customerInfo ->
val isPremium = customerInfo.entitlements["premium"]?.isActive == true
updatePremiumStatus(isPremium)
}
Step 5B (Parallel Migration) — Run RevenueCat Alongside Existing GPBL Code
If you want to keep your existing purchase code for now, configure the SDK with purchasesAreCompletedBy = MY_APP:
Purchases.configure(
PurchasesConfiguration.Builder(this, "your_google_play_api_key")
.appUserID("user_123")
.purchasesAreCompletedBy(PurchasesAreCompletedBy.MY_APP)
.build()
)
In this mode, RevenueCat observes and records purchases without interfering with your BillingClient flow. Your existing acknowledgement and purchase logic continues to work unchanged.
After your existing code successfully processes a purchase, call syncPurchases() to ensure RevenueCat is up to date:
// Call once after your own purchase handling succeeds
Purchases.sharedInstance.syncPurchasesWith { customerInfo ->
Log.d("RC", "Synced. Premium active: ${customerInfo.entitlements["premium"]?.isActive}")
}
Step 6: Handle Existing Subscribers
New users are covered the moment they make their first purchase through RevenueCat. Your existing subscribers are the challenge.
Option A: Client-Side Sync on First Launch
When a user opens the new version of your app for the first time, check whether RevenueCat already knows about their subscription. If not, call syncPurchases():
// In your post-login or post-launch flow — only once per user
val prefs = getSharedPreferences("rc_migration", MODE_PRIVATE)
val alreadySynced = prefs.getBoolean("synced_${userId}", false)
if (!alreadySynced) {
Purchases.sharedInstance.syncPurchasesWith { customerInfo ->
prefs.edit().putBoolean("synced_${userId}", true).apply()
Log.d("RC", "Migration sync complete for $userId")
}
}
This queries the local BillingClient for currently owned subscriptions and sends them to RevenueCat. It only covers subscribers who open the new app version. Source
Warning: With Android SDK v9 (backed by Play Billing Library 8),
syncPurchases()can only import active subscriptions and unconsumed one-time purchases. Expired subscriptions are no longer queryable from the BillingClient. Source
Option B: Server-Side Import (Best for Complete Coverage)
If you've been storing purchaseToken values on your backend, use RevenueCat's POST /receipts REST API to bulk-import them server-side. This backfills every subscriber regardless of whether they reopen the app:
curl -X POST https://api.revenuecat.com/v1/receipts \
-H "Authorization: Bearer YOUR_SECRET_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"app_user_id": "user_123",
"fetch_token": "GOOGLE_PURCHASE_TOKEN",
"product_id": "premium_monthly",
"is_restore": false
}'
RevenueCat deduplicates tokens automatically, so it's safe to forward new purchases continuously while bulk-importing old ones in parallel. Source
Option C: Google Historical Import (For Full Subscription History)
Google Play receipts expired more than 60 days ago cannot be imported via the API, and tokens only expose their current state — not their full renewal history. For rich historical data going back to July 2023, use RevenueCat's Google Historical Import:
- In Google Play Console, navigate to Download reports → Financial.
- Select Copy Cloud Storage URI next to "Estimated sales reports". Extract the
{bucket_id}from the URI (e.g.,pubsite_prod_rev_01234567890987654321). - Paste the bucket ID into Play Store Financial Reports Bucket ID in your RevenueCat Play Store app settings.
- Contact RevenueCat Support via the dashboard Contact Us form to trigger the one-time import.
Limitations: Historical import generates RevenueCat anonymous IDs for any purchase it wasn't already tracking, does not fire third-party integration events, and won't capture billing issues, partial refunds, or auto-renewal status changes. Source
Step 7: Test Before Production Cutover
Sandbox Setup
RevenueCat testing on Google Play requires a closed-track build and a licensed test account. Source
- In Google Play Console → Settings → License testing, add your test Google account.
- Create a Closed testing track and add your test account to the testers list.
- Open the tester opt-in URL on your test device — this is mandatory, not optional.
- Upload a signed APK or AAB to the closed track (it doesn't need to be rolled out).
- Make sure your device has a PIN (required for subscription purchases in sandbox).
⚠️ Only be logged into one Google account on your test device during sandbox testing. Multiple accounts cause silent purchase failures. Source
Sandbox subscription renewals are heavily accelerated:
| Production period | Sandbox renewal |
|---|---|
| 1 week | 5 minutes |
| 1 month | 5 minutes |
| 3 months | 10 minutes |
| 6 months | 15 minutes |
| 1 year | 30 minutes |
Test Checklist
Before cutting over to production, verify: Source
- [ ] Products load correctly in your paywall (non-empty offerings)
- [ ] A sandbox purchase succeeds and appears in the RevenueCat dashboard
- [ ] The
"premium"entitlement becomes active after purchase - [ ]
UpdatedCustomerInfoListenerfires and your UI updates - [ ] The View Sandbox Data toggle is enabled in the RevenueCat dashboard so you can see test transactions
- [ ] Subscription expiry (after the accelerated sandbox period) revokes the entitlement
- [ ] Restore purchases works from a fresh install
- [ ] Your release build uses the Google Play API key, not the Test Store key
Production Cutover Strategy
A safe cutover minimizes risk to active subscribers:
- Run parallel before cutting over. Deploy the first version with RevenueCat in Path B (parallel mode). Let real users sync their subscriptions over a few days via
syncPurchases(). - Import server-side. If you have purchase tokens stored, run the batch
POST /receiptsimport while parallel mode is live. - Optionally trigger Google Historical Import. This captures subscribers who haven't opened the app recently and fills in pre-July-2023 gaps.
- Switch to full SDK mode. Once your RevenueCat subscriber count matches your legacy system, remove the old
BillingClientcode and setpurchasesAreCompletedByback to the default. - Monitor for 48 hours. Charts and customer lists may take up to 24 hours to fully reflect imported data. Source
What You've Eliminated
After this migration is complete, you no longer need to maintain:
- Manual
BillingClientconnection and reconnection logic - Purchase acknowledgement tracking and retry
- Google Play Developer API OAuth2 integration for receipt validation
- Your own subscription state machine (active, grace period, paused, cancelled)
- Version-by-version GPBL API compatibility shims
RevenueCat handles all of it and keeps it working across GPBL major versions — including the v8 breaking changes that removed expired subscription queries. Source
Sources
- Migrate to RevenueCat – Migration Paths
- Android SDK Installation
- Configuring the SDK
- SDK Quickstart
- Configuring Products, Entitlements, and Offerings
- Displaying Products
- Making Purchases
- Getting Subscription Status (CustomerInfo)
- Using the SDK with Your Own IAP Code (formerly Observer Mode)
- Importing Historical Purchases
- Google Historical Import
- Google Play Store Sandbox Testing
- App Subscription Launch Checklist
- Android Native 8.x to 9.x Migration (GPBL 8)