If you've shipped an Android app with raw Google Play Billing Library (GPBL), you know the pain intimately: a BillingClient that silently disconnects mid-session, ITEM_ALREADY_OWNED errors you never expected, a 3-step acknowledgment dance that must complete within 3 days or Google auto-refunds the purchase, and a server-side receipt validation system you had to build and maintain yourself. Then Google changed the subscription model in May 2022, added base plans and offers, and your carefully tuned GPBL code needed yet another rewrite.
This guide walks you through migrating to RevenueCat — a battle-tested SDK and backend that handles all of that — without blowing up your existing subscriber base in the process. We'll cover both migration paths, real Kotlin code, and the critical (usually skipped) topic of existing subscribers.
1. The Real Pain of Raw Google Play Billing Library
Before reaching for RevenueCat, it's worth naming the exact bugs that send teams there.
BillingClient Lifecycle Management
BillingClient must be connected before every API call, but the connection is not persistent. You must call startConnection(), wait for onBillingSetupFinished(), handle onBillingServiceDisconnected() and retry — and do all of this correctly across Activity rotations, background/foreground transitions, and process death. Miss any edge case and your users hit BILLING_UNAVAILABLE or SERVICE_DISCONNECTED silently.
Purchase Acknowledgment Race Conditions
Every non-consumable purchase must be acknowledged within 3 days via acknowledgePurchase() (or consumePurchase() for consumables) or Google will automatically refund it. In a typical GPBL integration:
- Your app receives the
Purchaseobject inonPurchasesUpdated() - You must send the token to your server for validation
- Your server acknowledges via the Publisher API or your client calls
acknowledgePurchase() - If the user kills the app between steps 2 and 3, the token may never be acknowledged
This race condition is real and has cost developers real revenue.
Server-Side Receipt Validation Complexity
Google's Publisher API requires OAuth 2.0 credentials, token refresh logic, and handling of quota limits (the default quota is just 200,000 requests/day — easy to blow past at scale). You must build, host, and maintain this infrastructure. If your credentials rotate or a service account key expires, validation silently fails.
Pending Transactions
Since Android Q, users can initiate purchases that require out-of-band confirmation — paying cash at a kiosk, for example. GPBL delivers these as Purchase.PurchaseState.PENDING. Most teams don't handle this state at all, resulting in purchases that are stuck or lost.
The SDK Version Treadmill
Google has released BillingClient 5, 6, 7, and 8 — each with breaking changes. BillingClient 6 removed the old subscription upgrade/downgrade API; BillingClient 7 removed ProrationMode; BillingClient 8 removed APIs to query expired subscriptions and consumed one-time products entirely. Keeping up requires constant maintenance with zero business value.
RevenueCat handles all of this: connection management, acknowledgment, server-side validation, pending transactions, and GPBL version upgrades — so your team can build features instead.
2. Two Migration Paths
RevenueCat offers two distinct integration modes. Choosing the right one depends on whether you want to rip out your existing GPBL code immediately or run both systems in parallel.
Path A: Full SDK Takeover
RevenueCat's SDK replaces your GPBL code entirely. The SDK calls BillingClient internally, handles acknowledgment, validates with RevenueCat's servers, and exposes a clean Purchases API. This is the cleanest long-term state and is the right choice for greenfield or small apps.
In this mode, purchasesAreCompletedBy defaults to REVENUECAT — the SDK acknowledges purchases automatically.
Path B: SDK with Your Own IAP Code (PurchasesAreCompletedBy.MY_APP)
This is the key insight most migration articles skip. If you have existing subscribers, existing GPBL purchase flows, or a team that can't migrate everything at once, you can run RevenueCat alongside your existing code.
When purchasesAreCompletedBy is set to MY_APP, RevenueCat observes purchases but does not acknowledge them — your existing code still does that. RevenueCat still:
- Validates receipt tokens with Google's servers
- Updates subscriber state and entitlements in its backend
- Provides CustomerInfo with entitlement status
- Powers analytics, webhooks, and integrations
⚠️ Important: This setting was formerly called "Observer Mode." The term still appears in older SDK documentation, but current SDK versions use
PurchasesAreCompletedBy. Source
This path lets you ship RevenueCat in a single release without rewriting any purchase flow. Your existing acknowledgment code keeps running; RevenueCat gets visibility into every transaction. You can migrate purchase flows screen by screen over subsequent releases.
3. Android SDK Setup: Step-by-Step Kotlin
Step 1: Add the Gradle Dependency
In your module-level build.gradle (or build.gradle.kts):
// build.gradle.kts
dependencies {
implementation("com.revenuecat.purchases:purchases:9.+")
}
Check the latest release for the current version. RevenueCat SDK v9 uses Google Play Billing Library 8 internally. Source
SDK ↔ GPBL version map: | RevenueCat Android SDK | GPBL Version | |------------------------|-------------| | v6–v7 | BillingClient 5 | | v8 | BillingClient 7 | | v9 | BillingClient 8 |
Also ensure your AndroidManifest.xml includes the billing permission:
<uses-permission android:name="com.android.vending.BILLING" />
And set your Activity's launchMode to standard or singleTop — any other mode can cause purchases to be cancelled when the user backgrounds the app to complete bank verification. Source
Step 2: Configure the SDK on App Launch
In your Application.onCreate():
// MyApplication.kt
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 in debug builds only
Purchases.logLevel = if (BuildConfig.DEBUG) LogLevel.DEBUG else LogLevel.ERROR
Purchases.configure(
PurchasesConfiguration.Builder(
context = this,
apiKey = "your_google_play_api_key_here" // from RevenueCat Dashboard > API Keys
)
.appUserID("your_app_user_id") // pass null for anonymous IDs
.build()
)
}
}
🔑 Use your Google Play-specific public API key, found under Project Settings → API Keys → App specific keys in the RevenueCat dashboard. Never use the secret key on the client. Source
Step 3: Path B Only — Disable RevenueCat's Purchase Completion
If you're keeping your existing GPBL code (Path B), add one line to the builder:
import com.revenuecat.purchases.PurchasesAreCompletedBy
Purchases.configure(
PurchasesConfiguration.Builder(this, apiKey)
.appUserID(currentUserId)
.purchasesAreCompletedBy(PurchasesAreCompletedBy.MY_APP) // your code still acknowledges
.build()
)
With this set, RevenueCat will never call acknowledgePurchase() or consumePurchase() — your existing GPBL listener remains solely responsible for that. Source
Step 4: Enable Pending Transactions (Optional, SDK v8+)
If you want to support pending prepaid subscription transactions (introduced in SDK v8 / BillingClient 7), opt in explicitly:
Purchases.configure(
PurchasesConfiguration.Builder(this, apiKey)
.pendingTransactionsForPrepaidPlansEnabled(true)
.build()
)
Pending transactions are disabled by default. When enabled, RevenueCat handles the PENDING state correctly — no more lost cash-pay purchases. Source
Step 5: Listen for CustomerInfo Updates
// In your Activity or ViewModel
Purchases.sharedInstance.updatedCustomerInfoListener = UpdatedCustomerInfoListener { customerInfo ->
val isPremium = customerInfo.entitlements["premium"]?.isActive == true
updateUi(isPremium)
}
You can also fetch on demand:
Purchases.sharedInstance.getCustomerInfo { customerInfo, error ->
if (error == null) {
val isPremium = customerInfo?.entitlements?.get("premium")?.isActive == true
// gate your features here
}
}
4. Products, Entitlements, and Offerings in the Dashboard
Before your SDK can fetch products, you need to configure them in RevenueCat. The three-tier model — Products → Entitlements → Offerings — is the core of RevenueCat's architecture.
-
Products: Mirror your Google Play Console products. In the RevenueCat dashboard, navigate to your app and add products. For Google Play's post-2022 subscription model, you need both the subscription ID and the base plan ID. Source
-
Entitlements: Abstract access levels (e.g.,
premium,pro_tier). One entitlement can be unlocked by multiple products (annual, monthly, lifetime). This is what your app gates features against. -
Offerings: Groups of packages shown to the user. Your paywall fetches the current Offering and shows the packages inside it. Offerings let you A/B test pricing remotely without app updates.
SDK Version Compatibility for Google Subscriptions
RevenueCat SDK v5 and below only work with backwards-compatible Google Play products (flagged in Play Console). SDK v6+ supports all Google Play subscription configurations — base plans, offers, and all. If you have older app versions still in the wild, use the "SDK v6+ and backwards compatible" setting in your RevenueCat app settings to serve a fallback product to old SDK users. Source
5. Handling Existing Subscribers — The Critical Section
This is where most migrations fail. You cannot simply ship the RevenueCat SDK and assume your existing subscribers will "just work." Here's why:
- RevenueCat has never seen these purchase tokens before
- Until a token is submitted to RevenueCat's backend, those users appear as "no active subscription"
- Showing a paywall to your loyal paying subscribers because RevenueCat doesn't know about them is catastrophic
You have three options, and you should combine them:
Option 1: Client-Side Sync (SDK syncPurchases())
When an existing subscriber opens the new version of your app for the first time, call syncPurchases():
// Only trigger once, e.g., using a SharedPreferences flag
val prefs = getSharedPreferences("migration", MODE_PRIVATE)
if (!prefs.getBoolean("rc_synced", false)) {
Purchases.sharedInstance.syncPurchases()
prefs.edit().putBoolean("rc_synced", true).apply()
}
This queries BillingClient for currently owned subscriptions and non-consumed purchases and sends them to RevenueCat. Do not call this on every launch — it adds latency and can alias users incorrectly. Source
⚠️ GPBL 8 / SDK v9 limitation: Play Billing Library 8 removed the ability to query expired subscriptions and consumed one-time products. syncPurchases() on SDK v9 will only find active subscriptions and unconsumed purchases — not historical data. Source
Option 2: Server-Side Receipt Import (POST /receipts)
If your backend has been storing Google Play purchase tokens (you should be!), use RevenueCat's POST /receipts REST API to import them:
POST https://api.revenuecat.com/v1/receipts
Authorization: Bearer YOUR_SECRET_API_KEY
X-Platform: google
Content-Type: application/json
{
"app_user_id": "user_12345",
"fetch_token": "GOOGLE_PURCHASE_TOKEN",
"product_id": "your_subscription_id"
}
RevenueCat validates each token, creates or updates the subscriber record, and activates entitlements. This approach works even before your users update the app. Source
Rate limits apply — the API has quotas, so batch your imports with delays for large sets.
For tokens that have been expired fewer than 60 days ago, the API can import them. Tokens expired longer than 60 days cannot be imported this way due to Google's own API limitations. Source
Option 3: Google Historical Import (Large Migrations)
For apps with a large subscriber base and deep purchase history, RevenueCat offers Google Historical Import — a specialized process that pulls data directly from Google Play's financial sales reports, bypassing the 60-day (and 90-day token) expiry limitation. This can ingest subscription history dating back to July 2023. Source
Setup steps:
-
Get your Google Cloud Storage bucket ID: In Google Play Console, navigate to Download reports → Financial, and copy the Cloud Storage URI. Extract the bucket ID, which looks like
pubsite_prod_rev_01234567890987654321. -
Upload the bucket ID to RevenueCat: In your RevenueCat Play Store app settings, paste it under "Play Store Financial Reports Bucket ID" and save.
-
Ensure service credentials have Financial Access: Your Google Service Credentials in RevenueCat must have financial access granted or the import will fail or be delayed.
-
Enable Google Real-Time Developer Notifications: Required prerequisite.
-
Contact RevenueCat: Reach out via the dashboard Contact Us form to kick off the one-time import. Source
Limitations to know: Historical Import does not dispatch third-party integration events (webhooks, Amplitude, Mixpanel, etc.) for imported transactions. It also won't detect billing issues, partial refunds, or auto-renewal status for historical records. App User IDs for previously untracked purchases will be RevenueCat anonymous IDs.
The recommended combination for large migrations:
- Enable Google Historical Import for pre-migration history
- Use server-side POST /receipts for active subscribers you have tokens for
- Use client-side syncPurchases() as a safety net for users who update the app
6. Sandbox Testing Strategy
Never test billing logic against production. Source
Setup Checklist
-
Add license testers: In Google Play Console → Settings → License testing, add the Google account you'll use on your test device. This account will get test cards instead of real charges.
-
Create a closed test track: Upload a signed APK/AAB to an internal or closed testing track. Your app must be published to a track (doesn't need to be rolled out) before purchases work.
-
Add testers to the track: Add your test account to the tester list, then open the opt-in URL on your device. Skipping the opt-in URL is the #1 reason products fail to load in sandbox.
-
One account only: Be logged into your test device with only one Google account — multiple accounts in sandbox causes purchases to fail. This is a sandbox-only limitation; production users can have multiple accounts.
-
Set a device PIN: Without a PIN, subscriptions may fail with a cryptic "Something went wrong" message.
Accelerated Subscription Renewals
In sandbox, subscriptions renew on an accelerated schedule, maxing out at 6 renewals:
| Production Period | Sandbox Renewal |
|---|---|
| 1 week | 5 minutes |
| 1 month | 5 minutes |
| 3 months | 10 minutes |
| 6 months | 15 minutes |
| 1 year | 30 minutes |
Use this to test renewal flows, grace periods, and cancellation handling.
Verifying in RevenueCat Dashboard
After a sandbox purchase, enable "View Sandbox Data" in the RevenueCat dashboard nav bar. The transaction should appear immediately. If it doesn't appear, RevenueCat is not receiving the token — the most common causes are wrong API key, missing service credentials, or purchasesAreCompletedBy.MY_APP set without calling any sync method.
7. Production Cutover Checklist
A safe cutover is incremental. Don't flip every user at once.
Pre-Cutover
- [ ] Service credentials configured: Google Play service credentials are uploaded to RevenueCat and have financial data access. Invalid credentials are the #1 cause of receipt validation failures.
- [ ] Server notifications enabled: Enable Google Real-Time Developer Notifications in RevenueCat — this is how RevenueCat learns about renewals, cancellations, and billing issues without waiting for the app to open.
- [ ] Entitlements tested: Verify
CustomerInfo.entitlements["your_key"]?.isActivereturnstrueafter a sandbox purchase. - [ ] Existing subscriber import planned: Don't ship without a plan for your existing base. At minimum, have client-side
syncPurchases()wired in. - [ ] API key per build type: Never ship a Test Store API key to production. Use build config:
kotlin val rcApiKey = if (BuildConfig.DEBUG) BuildConfig.RC_TEST_KEY else BuildConfig.RC_PROD_KEY - [ ] SDK v9 / GPBL 8 awareness: If upgrading to SDK v9, note that consumed one-time purchases and expired subscriptions are no longer queryable. If you have lifetime purchase products, ensure they are configured as non-consumable in the RevenueCat dashboard before upgrading — otherwise they may be consumed and become unrestorable. Source
Cutover Strategy
Feature flag rollout (recommended): Gate the RevenueCat code path behind a remote config flag (Firebase Remote Config, LaunchDarkly, etc.). Start at 1–5% of users, monitor error rates and subscription status accuracy, then ramp to 100% over days.
Monitoring: Watch for:
- PURCHASE_NOT_ALLOWED errors (credential issues)
- Entitlement unlock failures (product ID mismatches between Play Console and RevenueCat)
- syncPurchases latency spikes (throttle the call if needed)
Rollback: Because Path B (PurchasesAreCompletedBy.MY_APP) keeps your existing GPBL code intact, rollback is simply disabling the feature flag — your GPBL code still works. If you went with Path A (full takeover), ensure you have a tested rollback build ready before 100% rollout.
Summary
| Concern | Raw GPBL | RevenueCat |
|---|---|---|
| BillingClient lifecycle | Your problem | SDK handles it |
| Purchase acknowledgment | Must implement, 3-day deadline | Auto (or your code in Path B) |
| Server-side validation | Build & maintain yourself | RevenueCat servers |
| Pending transactions | Usually unhandled | Supported in SDK v8+ |
| GPBL version upgrades | Breaking changes every release | RevenueCat absorbs them |
| Existing subscribers | Manual token import | syncPurchases(), REST API, or Historical Import |
| Analytics & webhooks | Build yourself | Built-in |
RevenueCat's PurchasesAreCompletedBy.MY_APP mode is the bridge that makes migration safe — you get RevenueCat's backend, analytics, and entitlement management from day one, without touching a single line of your existing purchase flow. Ship it, verify it, then migrate the purchase flows at your own pace.
Sources
- Using the SDK with your own IAP Code (Observer Mode / PurchasesAreCompletedBy)
- Google Historical Import
- Importing your Historical Purchases (Server-Side & Client-Side)
- Bulk Imports / Receipt Import API
- Android Native 7.x to 8.x Migration (BillingClient 7, Pending Transactions)
- Android Native 8.x to 9.x Migration (BillingClient 8)
- Google Subscriptions and Backwards Compatibility (SDK v6+ vs v5)
- Android SDK Installation
- Configuring the SDK
- SDK Quickstart
- Google Play Store Sandbox Testing
- Migrate to RevenueCat — Migration Paths Overview