RevenueCat Products Not Found / Not Available

12:00

What This Error Means

When RevenueCat reports "no products found" or "products not available", it means the SDK fetched your current offering from the RevenueCat backend but could not match the product identifiers to real products in the App Store or Google Play. The result is an empty or nil offerings object, which prevents your paywall from displaying prices or accepting purchases.

This is one of the most frequently encountered errors during development and is almost always a configuration issue — either in the store console, the RevenueCat dashboard, or your Xcode scheme settings.

Note: This error does not prevent your app from launching. It only means the paywall cannot show products. Your app can still function; users just can't subscribe until the configuration is corrected.

Error Messages

The exact error text varies by platform and log level:

text
# RevenueCat SDK debug log
Offerings: No products found

# StoreKit (iOS)
StoreKit: Products request failed - invalid product identifiers
Error: no products available to be fetched - check your product configuration

# Google Play Billing (Android)
BillingClient: Product not found - SKU is not available for purchase
BillingResponseCode: ITEM_UNAVAILABLE (4)

# RevenueCat verbose log (both platforms)
[Purchases] - DEBUG: No products found for offering "default"
[Purchases] - DEBUG: Fetched 0 products from the store

Root Causes

There are six distinct causes, each requiring a different fix:

  1. Products not created or not active in the store

    In App Store Connect, products must have "Approved" or "Ready to Submit" status and be available for sale. In Google Play Console, in-app products must be "Active". Products that are "Draft" or "Deactivated" will not be returned by the store API.

  2. Product IDs don't match between the store and RevenueCat

    The product identifier entered in RevenueCat's Product Catalog must match the product ID in App Store Connect or Google Play Console character-for-character, including case. A typo such as com.myapp.monthly vs com.myapp.Monthly will silently return zero products.

  3. App not published (Android requirement)

    Google Play requires the app to be uploaded to at least the internal testing track before the Billing API will return products. Unlike iOS, where you can test with a StoreKit configuration file in the simulator, Android billing always calls the live Google Play servers.

  4. StoreKit configuration file not set up for development (iOS)

    When running in the Simulator or testing locally on a device, iOS has no connection to App Store Connect. Without a StoreKit Configuration File enabled in your Xcode scheme, all product requests return empty. This is the most common cause for developers seeing "no products found" for the very first time.

  5. Entitlement not linked to any package or product in RevenueCat

    In the RevenueCat dashboard, each offering is made up of packages, and each package must be linked to a product. If you created an entitlement and a product but never added the product to a package inside an offering, RevenueCat will return an offering with zero packages — rendering the paywall empty.

  6. No offering set as "current" in the RevenueCat dashboard

    RevenueCat's getOfferings() API returns a special current offering that your paywall should display. If no offering is designated as current in the dashboard, the current property of the returned Offerings object will be nil.

Platform-Specific Fixes

iOS Fix

1. Check App Store Connect product status:

  1. Log in to App Store Connect and open your app.
  2. Navigate to In-App Purchases or Subscriptions.
  3. Confirm each product shows status Approved or Ready to Submit. Products in Draft state are invisible to StoreKit.
  4. Check that the product is marked as Available for Sale.

2. Set up a StoreKit Configuration File for local development:

  1. In Xcode, go to File → New → File and choose StoreKit Configuration File.
  2. Add each subscription or in-app purchase product, using the same product IDs as in App Store Connect and RevenueCat.
  3. Go to Edit Scheme → Run → Options and set StoreKit Configuration to your new file.
  4. Products will now resolve in Simulator without needing live App Store products.

3. Verify bundle ID: In Xcode's Signing & Capabilities tab, confirm the Bundle Identifier matches App Store Connect exactly.

Android Fix

1. Publish your app to at least internal testing:

  1. In Google Play Console, go to Release → Internal testing.
  2. Upload a signed APK or AAB (it does not need to be reviewed).
  3. Add your test Google account as an internal tester.
  4. Accept the testing invite link on your device. Products will now resolve.

2. Verify package name: Confirm the applicationId in your build.gradle (Module level) matches the package name in Google Play Console and in your RevenueCat project settings exactly.

3. Check product status in Google Play Console: Go to Monetize → Products → In-app products or Subscriptions and confirm each product's status is Active.

RevenueCat Dashboard Fix

Follow this checklist in the RevenueCat dashboard:

  1. Go to Product Catalog → Products. Verify each product's identifier matches the store exactly.
  2. Go to Product Catalog → Entitlements. Confirm your entitlement has at least one package attached.
  3. Go to Product Catalog → Offerings. Open your offering and verify packages are listed and each package has a product assigned.
  4. In the Offerings list, confirm one offering has a current tag. If not, click the three-dot menu on your offering and select Set as current.
Checklist summary: Product in store (Active) → Product ID in RevenueCat matches → Product assigned to Package → Package in Offering → Offering set as Current.

Debug with Code

Use the following snippets to log exactly what RevenueCat receives when fetching offerings. This is the fastest way to confirm whether the issue is in the store, RevenueCat config, or your code:

swift
// Swift — Debug offerings fetch
Purchases.logLevel = .debug  // Enable verbose logging first

Purchases.shared.getOfferings { offerings, error in
    if let error = error {
        print("Offerings error: \(error.localizedDescription)")
        return
    }

    guard let offerings = offerings else {
        print("Offerings object is nil — check RevenueCat dashboard configuration")
        return
    }

    print("All offerings: \(offerings.all.keys.joined(separator: ", "))")

    guard let current = offerings.current else {
        print("No current offering set — set one in the RevenueCat dashboard")
        return
    }

    print("Current offering: \(current.identifier)")
    print("Packages count: \(current.availablePackages.count)")

    if current.availablePackages.isEmpty {
        print("No packages in current offering — add products to packages in RevenueCat dashboard")
    } else {
        for package in current.availablePackages {
            print("Package: \(package.identifier), Product: \(package.storeProduct.productIdentifier), Price: \(package.storeProduct.localizedPriceString)")
        }
    }
}
kotlin
// Kotlin — Debug offerings fetch
Purchases.debugLogsEnabled = true  // Enable verbose logging first

Purchases.sharedInstance.getOfferingsWithCompletion(
    onError = { error ->
        Log.e("RevenueCat", "Offerings error: ${error.message} (code: ${error.code})")
    },
    onSuccess = { offerings ->
        Log.d("RevenueCat", "All offering IDs: ${offerings.all.keys.joinToString()}")

        val current = offerings.current
        if (current == null) {
            Log.w("RevenueCat", "No current offering — set one in RevenueCat dashboard")
            return@getOfferingsWithCompletion
        }

        Log.d("RevenueCat", "Current offering: ${current.identifier}")
        Log.d("RevenueCat", "Packages count: ${current.availablePackages.size}")

        if (current.availablePackages.isEmpty()) {
            Log.w("RevenueCat", "No packages found — add products to packages in RevenueCat dashboard")
        } else {
            current.availablePackages.forEach { pkg ->
                Log.d("RevenueCat", "Package: ${pkg.identifier}, " +
                    "Product: ${pkg.product.sku}, " +
                    "Price: ${pkg.product.price}")
            }
        }
    }
)