You've integrated RevenueCat, pushed a build to TestFlight or an internal track — and nothing works. Offerings are empty, purchases silently fail, entitlements never unlock. These issues are frustrating precisely because they rarely throw loud exceptions; they just quietly don't work.

This guide walks through the five most common RevenueCat integration problems, gives you a systematic debugging approach starting with logs, and provides platform-specific code for Swift, Kotlin, and Flutter.


Step 0: Always Enable Debug Logs First

Before diagnosing anything, turn on debug logs. This is the single most important step in any RevenueCat troubleshooting session.

"Debug logs contain important information about what's happening behind the scenes and should be the first thing you check if your app is behaving unexpectedly." — RevenueCat Configuring SDK docs

Set the log level before you call Purchases.configure(...):

Swift:

// Set BEFORE configure
Purchases.logLevel = .debug
Purchases.configure(withAPIKey: "your_public_api_key")

Kotlin:

// Set BEFORE configure
Purchases.logLevel = LogLevel.DEBUG
Purchases.configure(PurchasesConfiguration.Builder(context, "your_public_api_key").build())

Flutter (Dart):

// Set BEFORE configure
await Purchases.setLogLevel(LogLevel.debug);
await Purchases.configure(PurchasesConfiguration("your_public_api_key"));

All SDK log lines are prefixed with [Purchases], making them easy to filter in Xcode's console or Android's Logcat. Watch for lines tagged with 🐞 (RevenueCat errors), 🍎 (Apple errors), and 🤖 (Google errors) — and pay special attention to any ‼️ double-exclamation entries, which flag issues requiring your immediate attention. Source

iOS Gotcha: If you've enabled debug logs but see no output in Xcode, go to Product → Scheme → Edit Scheme... and make sure the OS_ACTIVITY_MODE environment variable is not disabled. Disabling it will silently suppress all SDK logs. Source

With logs flowing, you're ready to diagnose specific problems.


Issue 1: Empty Offerings

Symptom: getOfferings() returns an empty result or a nil current offering. Your paywall shows nothing.

Why it happens: RevenueCat fetches product identifiers from its API, then asks the store (App Store / Play Store) to hydrate them with pricing data. If the store can't find a product, it silently drops it. The result: RevenueCat has no products to serve. Source

The debug log will tell you directly:

There's a problem with your configuration. None of the products registered
in the RevenueCat dashboard could be fetched from the [Play Store/App Store].

Checklist:

  1. Product IDs match exactly — The product identifier in the RevenueCat dashboard must be an exact, case-sensitive match to the identifier in App Store Connect or the Google Play Console. A single typo means zero products.
  2. Products are attached to an Offering — Creating a product in the dashboard is not enough; it must be added to an Offering and that Offering must be set as the default. Source
  3. Products are in an "Active" / "Ready to Submit" state — On Google Play, subscriptions must be Active. On App Store Connect, products must be in Ready to Submit or Approved status. Draft products are invisible to the SDK.
  4. A signed APK/AAB has been uploaded (Android) — Google Play won't serve products for an app that has never had a build uploaded. Source
  5. In-App Purchase capability is enabled (iOS) — In Xcode, go to Project Target → Capabilities → In-App Purchase and make sure it's toggled on. Source

Code pattern to safely handle empty offerings:

// Swift
Purchases.shared.getOfferings { offerings, error in
    if let error = error {
        print("Error fetching offerings: \(error.localizedDescription)")
        return
    }
    guard let current = offerings?.current else {
        print("No current offering found — check RevenueCat dashboard configuration")
        return
    }
    // Present your paywall with `current`
}
// Kotlin
Purchases.sharedInstance.getOfferingsWith(
    onError = { error -> Log.e("RC", "Offerings error: ${error.message}") },
    onSuccess = { offerings ->
        val current = offerings.current
        if (current == null) {
            Log.w("RC", "No current offering — check dashboard configuration")
            return@getOfferingsWith
        }
        // Present paywall with `current`
    }
)
// Flutter (Dart)
try {
  Offerings offerings = await Purchases.getOfferings();
  if (offerings.current == null) {
    debugPrint('No current offering — check RevenueCat dashboard');
    return;
  }
  // Present paywall with offerings.current
} on PlatformException catch (e) {
  debugPrint('Error fetching offerings: ${e.message}');
}

Tip: RevenueCat's Debug UI (iOS SDK 4.22.0+, Android SDK 6.9.2+) renders an overlay in your app showing every configured Offering and its products, letting you validate configuration without reading raw logs. Source


Issue 2: Invalid Product Identifiers

Symptom: You see Invalid Product Identifiers in your debug logs, or a subset of products is missing from an otherwise-populated Offering.

Why it happens: When the SDK fetches products from the store, the store returns two lists: the products it found and the identifiers it didn't recognize. The RevenueCat SDK logs the unrecognized ones as Invalid Product Identifiers. Source

What to check:

  • Bundle ID match — On iOS, the bundle ID in your Xcode project must exactly match the App ID in App Store Connect. On Android, the application ID in build.gradle must match the package name in the Play Console.
  • Product ID format — iOS product IDs are often namespaced (e.g., com.yourapp.monthly). The full string, including prefix, must match what's in both the store and the RevenueCat dashboard.
  • Environment mismatch — Sandbox products are only visible in sandbox builds. If you're running a production-signed build against sandbox credentials, products won't resolve.
  • iOS 18.4+ simulator bug — Apple introduced a known regression in iOS 18.4 simulators where SKProductsRequest returns empty arrays. Test on a physical device or use the RevenueCat Test Store instead of the live App Store sandbox when on an affected simulator version. Source

Scan logs for this line:

Invalid Product Identifiers: ["com.yourapp.monthly_typo"]

Each identifier in that array tells you exactly which products failed store lookup. Fix the identifier in whichever system has the typo — RevenueCat dashboard or store console.


Issue 3: Entitlements Not Unlocking After Purchase

Symptom: A user completes a purchase (no error thrown), but checking customerInfo.entitlements.active returns empty. The user paid, but your app is still showing the paywall.

Why it happens: There are three distinct root causes, each requiring a different fix.

3a. Product is not attached to an Entitlement

This is the most common cause. Creating an entitlement in the RevenueCat dashboard is not enough — you must explicitly attach your products to it. Without attachment, a successful purchase will not grant the entitlement.

"Failing to add your products to an entitlement could lead to your users making purchases that don't unlock access to the promised content." — RevenueCat Entitlements docs

Fix: In your RevenueCat Project dashboard, go to Product Catalog → Entitlements, open your entitlement (e.g., pro), and click Attach to link every product that should unlock it.

3b. Reading stale cached CustomerInfo

The SDK caches CustomerInfo and updates the cache if it's older than 5 minutes — but only when you explicitly call getCustomerInfo(), make a purchase, or restore. If you check entitlement status using a stale object captured before the purchase completed, it will appear empty. Source

Fix: Always check CustomerInfo in the purchase completion callback, not from a previously cached variable:

// Swift — check entitlement in the purchase callback
Purchases.shared.purchase(package: package) { transaction, customerInfo, error, userCancelled in
    if let error = error { /* handle */ return }
    if customerInfo?.entitlements["pro"]?.isActive == true {
        // Unlock content now
    }
}
// Kotlin — check entitlement in the purchase callback
Purchases.sharedInstance.purchaseWith(
    purchaseParams = PurchaseParams.Builder(activity, packageToPurchase).build(),
    onError = { error, userCancelled -> /* handle */ },
    onSuccess = { _, customerInfo ->
        if (customerInfo.entitlements["pro"]?.isActive == true) {
            // Unlock content
        }
    }
)
// Flutter — check entitlement in the purchase callback
try {
  CustomerInfo customerInfo = await Purchases.purchasePackage(package);
  if (customerInfo.entitlements.active.containsKey('pro')) {
    // Unlock content
  }
} on PlatformException catch (e) {
  // handle error
}

3c. User identity changed

If the user reinstalled the app, logged out, or was assigned a new anonymous ID, their purchase history is associated with a different App User ID. The current user's CustomerInfo appears empty because it genuinely has no purchases tied to it.

Fix: Always implement a Restore Purchases button in your paywall or settings screen. Restoring re-syncs the underlying store receipt with the current App User ID. Source

// Flutter — restore purchases
try {
  CustomerInfo restoredInfo = await Purchases.restorePurchases();
  if (restoredInfo.entitlements.active.containsKey('pro')) {
    // Entitlements restored successfully
  }
} on PlatformException catch (e) {
  debugPrint('Restore error: ${e.message}');
}

Issue 4: Google Play Credential Delays (503 / 521 Errors)

Symptom: Android purchases fail with INVALID_CREDENTIALS errors immediately after you've set up Google Play Service Credentials in the RevenueCat dashboard. Everything looks correct, but it still doesn't work.

Why it happens: Google's API authorization pipeline has an inherent propagation delay. This is documented and expected.

"It can take up to 36 hours for your Play Service Credentials to work properly with the Google Play Developer API. You may see 'Invalid Play Store credentials' errors (503 or 521) and be unable to make purchases with RevenueCat until this happens." — RevenueCat Google Play Store credentials docs

What to do while you wait:

  1. Use this time to run the Google Play Checklists to confirm every step was completed:
  2. Both Google Play Android Developer API and Google Play Developer Reporting API are enabled in Google Cloud.
  3. The service account has Pub/Sub Editor and Monitoring Viewer roles.
  4. The service account was invited in Play Console under Users and Permissions with financial data and order management permissions, and status shows Active.
  5. The JSON key file is uploaded to the RevenueCat dashboard under Project Settings → Google Play App Settings.
  6. A signed APK or AAB has been uploaded to at least one track. Source

  7. Don't keep retrying the same credential upload hoping for a faster result — the 36-hour window is on Google's side, not RevenueCat's.

  8. Test your iOS integration in the meantime; the Apple and Google credential systems are independent.

Note on 503 vs 521: A 503 error typically indicates RevenueCat can't reach Google's API. A 521 is a Cloudflare origin error. Both usually resolve on their own within the propagation window if configuration is correct.


Issue 5: User Identity Problems

Symptom: Purchases appear to vanish after login/logout, different users see each other's subscriptions, or the same user has multiple unlinked customer profiles in the RevenueCat dashboard.

Why it happens: RevenueCat assigns every user an App User ID — either anonymous (auto-generated as $RCAnonymousID:...) or identified (your own user ID). If you don't manage the logIn / logOut lifecycle correctly, purchases end up fragmented across multiple IDs. Source

The correct identity lifecycle

On app launch (user not yet logged in): Don't pass any user ID. RevenueCat generates an anonymous ID automatically.

// Swift — anonymous initialization
Purchases.configure(withAPIKey: "your_public_api_key")
// RC auto-assigns $RCAnonymousID

When the user logs in to your system: Call logIn() with your own stable user ID. RevenueCat aliases the anonymous profile to your identified ID, so any purchases made anonymously are preserved.

// Swift — identify after login
Purchases.shared.logIn("your_stable_user_id") { customerInfo, created, error in
    // customerInfo now reflects the identified user's subscription state
}
// Kotlin — identify after login
Purchases.sharedInstance.logInWith(
    appUserID = "your_stable_user_id",
    onError = { error -> Log.e("RC", error.message) },
    onSuccess = { customerInfo, created ->
        // customerInfo reflects identified user
    }
)
// Flutter — identify after login
try {
  LogInResult result = await Purchases.logIn("your_stable_user_id");
  // result.customerInfo reflects identified user
} on PlatformException catch (e) {
  debugPrint('Login error: ${e.message}');
}

When the user logs out: Call logOut(). RevenueCat generates a new anonymous ID for the next session. Never call logIn() with the previous user's ID on behalf of the new user.

// Swift
Purchases.shared.logOut { customerInfo, error in
    // New anonymous session started
}

Common identity anti-patterns to avoid

❌ Anti-Pattern ✅ Fix
Hardcoding the same user ID for all users Use your auth system's stable UID, or omit for anonymous
Never calling logOut() when a user signs out Always call logOut() to start a clean anonymous session
Using email as an App User ID Emails change; use an immutable database UID
App User IDs longer than 100 characters Keep IDs under 100 characters; longer IDs trigger INVALID_APP_USER_ID errors Source

If a user's purchases have already been transferred to the wrong profile, they can Restore Purchases to re-claim their subscription under their current App User ID, depending on your project's configured restore behavior.


Quick-Reference Debugging Checklist

Before opening a support ticket, run through this list:

  • [ ] Debug logs are enabled and actively showing [Purchases] output
  • [ ] Product IDs in RevenueCat dashboard exactly match store console identifiers
  • [ ] Products are in Active / Ready to Submit state in the store
  • [ ] Products are attached to an Offering, and that Offering is set as Default
  • [ ] Products are attached to the correct Entitlement in the RevenueCat dashboard
  • [ ] For Android: a signed APK/AAB has been uploaded to at least one Play track
  • [ ] For Android new credentials: waited the full 36-hour propagation window
  • [ ] getCustomerInfo() is called fresh after purchase, not from a stale cached value
  • [ ] logIn() / logOut() are called at the correct points in your auth lifecycle
  • [ ] App includes a visible Restore Purchases button

Sources