What This Error Means

0:03:00

When RevenueCat reports "no products found", "empty offerings", or "Error fetching offerings", it means one specific thing: the SDK reached the RevenueCat backend and fetched your offering configuration, but somewhere along the chain the product identifiers could not be resolved into real, purchasable store products. The result is a nil current offering or an offering with zero availablePackages, and a paywall with nothing on it.

This is, by a wide margin, the most common RevenueCat issue. The good news: it is always a configuration problem with a finite list of causes, and every one of them is covered on this page. To fix it, you need to understand the chain the SDK walks every time you call getOfferings():

text
Store product (App Store / Google Play)        ← must exist, be active/approved & available
        ↓  product ID must match character-for-character
RevenueCat Product (Product Catalog → Products) ← must exist for YOUR platform
        ↓  must be attached to a package
Package (e.g. $rc_monthly)                      ← must contain a product for YOUR store
        ↓  must belong to an offering
Offering (e.g. "default")                       ← must be marked Current
        ↓  fetched via your public API key (appl_… / goog_…)
Your app                                        ← bundle ID / package name must match
        ↓  device store must be able to serve the products
The device                                      ← agreements signed, app published, tester set up

If any single link in that chain is broken, you get empty offerings. The error message usually tells you which half of the chain to look at: RevenueCat's side or the store's side.

The Error Message Zoo

All of the following are the same family of problem. Find yours:

text
# iOS: the classic
[RevenueCat] 🍎‼️ Error fetching offerings - The operation couldn't be completed.
There's a problem with your configuration. None of the products registered in the
RevenueCat dashboard could be fetched from App Store Connect (or the StoreKit
Configuration file if one is being used).
More information: https://rev.cat/why-are-offerings-empty

# Android: platform mismatch (ConfigurationError)
[RevenueCat] 😿‼️ Error fetching offerings -
PurchasesError(code=ConfigurationError, underlyingErrorMessage=You have configured
the SDK with a Play Store API key, but there are no Play Store products registered
in the RevenueCat dashboard for your offerings. If you don't want to use the
offerings system, you can safely ignore this message. To configure offerings and
their products, follow the instructions in https://rev.cat/how-to-configure-offerings.
More information: https://rev.cat/why-are-offerings-empty

# iOS: products exist but were never submitted
WARN: ⚠️ RevenueCat SDK is configured correctly, but contains some issues…
  ⚠️ monthly (monthly): This product's status (READY_TO_SUBMIT) requires you to
     take action in App Store Connect before using it in production purchases.

# Android: store can't serve the SKU
BillingClient: Product not found - SKU is not available for purchase
BillingResponseCode: ITEM_UNAVAILABLE (4)

# Both: silent variant (no error at all)
offerings.current == nil          // or
offerings.current.availablePackages.count == 0

# Both: network family (different root cause: connectivity, not configuration)
Error: Unable to fetch offerings: NetworkError / Request timed out
Note: This error never crashes your app. It only means the paywall can't show products. But it absolutely will reach production if the cause is an unsigned agreement or an unsubmitted product, so fix it properly rather than retrying around it.

5-Minute Triage

0:03:00

Before checking anything else, enable debug logging, since every diagnosis below starts from what the log says:

text
Swift:         Purchases.logLevel = .debug          (before configure)
Kotlin:        Purchases.logLevel = LogLevel.DEBUG  (before configure)
React Native:  Purchases.setLogLevel(LOG_LEVEL.DEBUG)
Flutter:       await Purchases.setLogLevel(LogLevel.debug)

Then match your symptom in this table. Each row links to the section with the verified fix:

Your symptomMost likely causeGo to
Log says ConfigurationError… Play Store API key, but there are no Play Store products registered (or the App Store equivalent)Your offering only contains products for the other storeDashboard Fixes, cause D
offerings.current is nil, no store error in the logNo offering marked Current, or no packages/products in itDashboard Fixes, causes A–C
"None of the products registered… could be fetched", iOS, first integration everPaid Applications Agreement not Active, or Simulator without a StoreKit Configuration fileiOS Fixes, causes 1 & 6
Log warns READY_TO_SUBMIT / products show "Missing Metadata"Products incomplete or never submitted with a buildiOS Fixes, causes 2 & 3
RevenueCat REST API / dashboard shows everything correct, but availablePackages is 0 on iOSNo binary ever uploaded, so StoreKit hasn't activated your product IDsiOS Fixes, cause 4
Works for you, fails for users in another countryProduct availability limited to specific countries/regionsiOS Fixes, cause 7 / Android Fixes, cause 6
Android: ITEM_UNAVAILABLE or empty offerings on a real deviceApp not on a testing track, tester not opted in, or installed build isn't from PlayAndroid Fixes, causes 1–3
Android: dashboard shows credential warnings, or setup was < 36h agoPlay service credentials invalid or still propagatingAndroid Fixes, cause 5
React Native in Expo Go: native module missing / empty productsExpo Go can't run native modules, so you need a dev buildCross-Platform
getProducts() works but getOfferings() is emptyBreak is between product and offering in the dashboardDashboard Fixes
Worked yesterday, empty today, nothing changedStore/cache propagation, or someone edited the offeringStill Stuck?
NetworkError / timeoutConnectivity, VPN/firewall, or (rarely) service disruption, not configurationStill Stuck?
Golden diagnostic: the debug log prints the products RevenueCat asked the store for and what came back. If the log shows your product IDs being requested but zero returned, the problem is on the store side (steps 04–05). If the log never even requests your product IDs, the problem is on the RevenueCat dashboard side (step 03).

RevenueCat Dashboard Fixes

0:04:00

Work through these in order in the RevenueCat dashboard. They cover every dashboard-side break in the chain.

A. One offering must be "Current"

getOfferings() returns the current offering in its current property. Go to Product Catalog → Offerings and confirm exactly one offering shows the Current badge. If not, open the offering's menu and select Make Current. Without it, offerings.current is nil even when everything else is perfect.

B. The current offering must have packages

Open the current offering. The Packages section must list at least one package (e.g. $rc_monthly, $rc_annual). An offering with zero packages returns an empty paywall with no error.

C. Every package needs a product for YOUR platform

Open each package and check the attached products. A package can hold an App Store product and a Google Play product, and it needs one for every platform you ship. A package with only an iOS product looks fine when you test on iOS and silently breaks on Android.

D. The ConfigurationError platform mismatch

This is the exact cause of the error below, and it's pure dashboard configuration:

text
PurchasesError(code=ConfigurationError, underlyingErrorMessage=You have configured
the SDK with a Play Store API key, but there are no Play Store products registered
in the RevenueCat dashboard for your offerings. …)

Translation: your app runs with a goog_ API key, but every product in your current offering's packages belongs to the App Store (or vice versa with an appl_ key and Play-only products). The store the SDK talks to has nothing to fetch. Fix:

  1. In Product Catalog → Products, click + New and create the product for the missing store (the Google Play product ID, e.g. premium_monthly:monthly-autorenew for subscriptions with base plans, or import it automatically).
  2. Open your current offering → each package → Edit → attach the new store's product next to the existing one.
  3. Re-run the app. The error disappears once every package has a product for the active store.

E. Product IDs must match the store character-for-character

In Product Catalog → Products, compare each identifier against App Store Connect / Google Play Console. The match is case-sensitive and whitespace-sensitive: com.app.monthlycom.app.Monthly, and a trailing space (easy to pick up when pasting) breaks it silently. When in doubt, delete the ID and re-paste it carefully.

F. The right API key, from the right project

  • Use the public per-platform key from Project Settings → API Keys: appl_… for App Store, goog_… for Play. Never the secret sk_… key in an app.
  • If you have multiple RevenueCat projects (e.g. staging/production), confirm the key belongs to the project whose dashboard you are editing. A key from the wrong project fetches that project's (empty) offerings, a classic "but I configured everything!" trap.
  • Bundle ID (iOS) / package name (Android) in Apps & providers must match what your build actually uses, including .debug / flavor suffixes (e.g. com.app.dev). A suffixed debug build won't match an app configured as com.app.
Checklist summary: store product exists → identical product ID in RevenueCat → product attached to package for your platform → package in offering → offering marked Current → correct public API key + matching bundle ID/package name.

iOS / App Store Fixes

0:05:00

Ordered by how often each one turns out to be the real cause, based on resolved community threads.

1. The Paid Applications Agreement (the #1 hidden cause)

Until this agreement is Active, Apple serves zero in-app purchase products to your app, including in sandbox. It also silently expires, which is how apps that "worked for months" suddenly show empty paywalls. Verified fix from multiple community threads:

  1. Go to App Store ConnectBusiness (formerly Agreements, Tax, and Banking).
  2. The Paid Applications Agreement must show Active: not "Pending", not "New", not expired. Accept the latest terms if prompted.
  3. Complete Banking Information and Tax Forms (W-9 for US, W-8BEN for non-US). The agreement isn't Active until both are done.
  4. Allow up to 24–48 hours for Apple's verification after completing it.

2. Product status: complete the metadata

In App Store Connect → your app → Subscriptions / In-App Purchases, each product must not be in Missing Metadata or Developer Action Needed. To get a product to a fetchable state it needs: a price for at least one region, at least one localization (display name + description), and a review screenshot. Ready to Submit is fine for sandbox/StoreKit testing; Approved is required for production.

3. Subscriptions must live in a Subscription Group

Apple requires every auto-renewable subscription to belong to a subscription group. If you created products via the API or in a hurry, confirm each one is assigned to a group. Ungrouped subscriptions don't resolve. (This was the verified fix in a widely-shared community write-up.)

4. Upload a binary once (StoreKit product activation)

The trap that catches brand-new apps: everything is configured correctly, the RevenueCat REST API returns your offerings, but availablePackages is 0 on device. Apple needs to process at least one uploaded binary to link new product IDs to your app. Archive and upload any build to App Store Connect (TestFlight is enough, no review needed), wait for processing (15 minutes to a few hours), and the products start resolving.

5. Bundle ID and capability

  • Xcode → target → Signing & Capabilities: the Bundle Identifier must match App Store Connect and RevenueCat → Apps & providers exactly.
  • Add the In-App Purchase capability to the target if it's missing.

6. Simulator needs a StoreKit Configuration file

The iOS Simulator does not talk to App Store Connect. Without a StoreKit Configuration file selected in your scheme, product requests return empty. This is the most common first-day experience. Either:

  1. File → New → File → StoreKit Configuration File, add your products with the exact same product IDs, then Product → Scheme → Edit Scheme → Run → Options → StoreKit Configuration → select the file; or
  2. Test on a physical device with a sandbox tester (Settings → App Store → Sandbox Account), keeping StoreKit Configuration set to None.
The reverse trap: a StoreKit Configuration file left enabled while you expect real App Store/sandbox products (or with product IDs that drifted from App Store Connect) also produces empty offerings. When testing against the sandbox, set StoreKit Configuration back to None.

7. Country / region availability

A verified community fix: subscriptions were only available in one country, so testers elsewhere got empty offerings. In App Store Connect, check each product's Availability and make sure it covers every country you (and your testers) are in. Remember your sandbox account also has a country.

8. Propagation time

New products and metadata edits can take from a couple of hours up to ~24 hours to propagate through Apple's systems. If you created the products minutes ago and every checklist item above passes, wait before changing anything else.

App Review note: Apple's review environment is notoriously flaky at fetching products. Always handle a failed offerings fetch gracefully (retry button, not a blank screen), and if a reviewer reports "no products", verify nothing above regressed and resubmit.

Android / Google Play Fixes

0:05:00

Unlike iOS, Android has no offline StoreKit equivalent: the Billing Library always talks to the live Google Play backend, so Play must know about your app, your products, and your tester. Ordered by frequency:

1. The app must be published to a testing track

Google Play returns no products for an app it has never seen. Upload a signed release build (AAB) to at least the Internal testing track (Play Console → Release → Testing → Internal testing) and make sure the track shows as available to testers. No review is required for internal testing.

2. The tester must be added AND opted in

This is the step everyone misses. Adding the Google account to the tester list is not enough:

  1. Add your Google account as a tester on the track.
  2. Open the opt-in link (Play Console shows it under "How testers join your test") on the device, signed in with that account, and tap Become a tester.
  3. Install the app at least once through Google Play via that link. After that, locally-built debug builds with the same applicationId and signature setup can fetch products too.

3. The build on the device must match Play's expectations

  • applicationId in build.gradle must match Play Console and RevenueCat → Apps & providers exactly. Watch out for .debug suffixes added by build types.
  • The device's primary Google Play account must be the tester account. Multiple-account devices often query Play with the wrong one; if in doubt, clear Play Store data or use a profile with only the tester account.
  • Emulators must include the Google Play Store image (not just "Google APIs"), with the tester signed in.

4. Products must be Active (and one-time products activated)

In Play Console → Monetize → Products, every subscription and in-app product referenced by your offering must show Active. Newly created in-app products start as drafts until you press Activate. For subscriptions, check the base plan is active too, since a subscription with no active base plan returns ITEM_UNAVAILABLE.

5. Service credentials: valid and propagated (up to 36 hours)

RevenueCat needs valid Play service credentials to validate purchases and (for imports) read your products. In RevenueCat → Apps & providers → Google Play, the credentials check must pass. Two verified gotchas:

  • Freshly-created service credentials can take up to 36 hours to propagate through Google's systems. If you finished the Play setup today and everything else checks out, this is likely your answer. Wait it out.
  • A quick way to force propagation along: edit any product's description in Play Console and save, which nudges Google's caches.

6. Country availability

Check the app's country availability (Play Console → Release → Production → Countries/regions) and each subscription's regional pricing. A tester in a country where the app or product isn't available gets empty results.

7. Still ITEM_UNAVAILABLE?

That response code means "Play knows the app but won't sell this SKU to this user". Re-walk causes 1–3 with this lens: right track? right account? installed from Play at least once? It almost always lands on one of those three.

Skip the queue while you wait: RevenueCat's Test Store for Android lets you build and test the entire purchase flow without any Play Console setup. It's useful while tracks publish and credentials propagate.

React Native, Flutter & Cross-Platform

0:03:00

Everything in steps 03–05 applies to React Native, Flutter, and KMP too. The SDK ultimately calls the same native StoreKit / Play Billing APIs. These causes are specific to cross-platform setups:

Expo Go cannot fetch products, ever

react-native-purchases is a native module, and Expo Go does not include it. Offerings/products will always come back empty there. Create a development build with EAS Build (npx expo run:ios / eas build --profile development) and test in that. The same applies to any environment that strips native modules.

Configure before you fetch (race conditions)

Calling getOfferings() before Purchases.configure() has completed (easy to do across JS module boundaries or in parallel useEffects) returns errors or empty results. Configure once, as early as possible (e.g. at app root), and await it where the API allows before fetching offerings.

Use the platform-correct key on each platform

Cross-platform apps need both keys, selected at runtime:

typescript
// React Native
import { Platform } from 'react-native';
import Purchases, { LOG_LEVEL } from 'react-native-purchases';

Purchases.setLogLevel(LOG_LEVEL.DEBUG);
Purchases.configure({
  apiKey: Platform.OS === 'ios' ? 'appl_YOUR_IOS_KEY' : 'goog_YOUR_ANDROID_KEY',
});

Passing the iOS key on Android (or vice versa) produces exactly the ConfigurationError covered in step 03.

The getProducts() vs getOfferings() diagnostic

A pattern reported across React Native threads: Purchases.getProducts([...]) returns your products, but getOfferings() is empty. This is actually good news. It proves the store side works, and isolates the break to the dashboard chain (packages → offering → current). Go back to step 03 and check causes A–D; on iOS also confirm the Paid Applications Agreement, which can break offerings while getProducts still partially works in some setups.

Flutter note: the equivalent calls are Purchases.getOfferings() / Purchases.getProducts() from purchases_flutter, and the same diagnostic applies. Hot reload does not re-run configure(). Do a full restart after changing keys.

Debug with Code

0:04:00

These snippets print exactly where the chain breaks: offerings object → current offering → packages → products. Run one, read the output, and it points you at the right section of this guide:

swift
// Swift: walk the chain
Purchases.logLevel = .debug  // before configure

Purchases.shared.getOfferings { offerings, error in
    if let error = error {
        print("❌ Offerings error: \(error.localizedDescription)")
        // ConfigurationError → step 03 (platform mismatch / dashboard)
        // Network errors    → step 08
        return
    }
    guard let offerings = offerings else {
        print("❌ Offerings object is nil → check API key & project (step 03-F)")
        return
    }
    print("All offerings: \(offerings.all.keys.joined(separator: ", "))")

    guard let current = offerings.current else {
        print("⚠️ No current offering → mark one Current (step 03-A)")
        return
    }
    print("Current offering: \(current.identifier)")

    if current.availablePackages.isEmpty {
        print("⚠️ 0 packages resolved → store could not serve the products (steps 04-05)")
        print("   (If the dashboard shows packages, this is store-side: agreement,")
        print("    product status, binary activation, testing track, or tester setup.)")
    } else {
        for package in current.availablePackages {
            print("✅ \(package.identifier) → \(package.storeProduct.productIdentifier) @ \(package.storeProduct.localizedPriceString)")
        }
    }
}
kotlin
// Kotlin: walk the chain
Purchases.logLevel = LogLevel.DEBUG  // before configure

Purchases.sharedInstance.getOfferingsWith(
    onError = { error ->
        Log.e("RC", "❌ Offerings error: ${error.message} (code: ${error.code})")
        // ConfigurationError → step 03 (platform mismatch / dashboard)
    },
    onSuccess = { offerings ->
        Log.d("RC", "All offerings: ${offerings.all.keys.joinToString()}")

        val current = offerings.current
        if (current == null) {
            Log.w("RC", "⚠️ No current offering → mark one Current (step 03-A)")
            return@getOfferingsWith
        }
        Log.d("RC", "Current offering: ${current.identifier}")

        if (current.availablePackages.isEmpty()) {
            Log.w("RC", "⚠️ 0 packages resolved → Play could not serve the products (step 05)")
            Log.w("RC", "   Check: testing track, tester opt-in, install-from-Play, Active products")
        } else {
            current.availablePackages.forEach { pkg ->
                Log.d("RC", "✅ ${pkg.identifier} → ${pkg.product.id} @ ${pkg.product.price.formatted}")
            }
        }
    }
)
typescript
// React Native: walk the chain (same logic for Flutter's purchases_flutter)
import Purchases, { LOG_LEVEL } from 'react-native-purchases';

Purchases.setLogLevel(LOG_LEVEL.DEBUG);

try {
  const offerings = await Purchases.getOfferings();
  console.log('All offerings:', Object.keys(offerings.all));

  if (!offerings.current) {
    console.warn('⚠️ No current offering → mark one Current (step 03-A)');
  } else if (offerings.current.availablePackages.length === 0) {
    console.warn('⚠️ 0 packages resolved → store-side issue (steps 04-05)');
  } else {
    offerings.current.availablePackages.forEach((pkg) =>
      console.log(`✅ ${pkg.identifier} → ${pkg.product.identifier} @ ${pkg.product.priceString}`),
    );
  }
} catch (e) {
  console.error('❌ Offerings error:', e);
  // ConfigurationError → step 03; native module missing in Expo Go → step 06
}

Reading the debug log

  • Configuring Purchases with API key: appl_/goog_…: confirm it's the key and platform you expect.
  • Requesting products from the store with identifiers: …: the dashboard chain works; the question is now what the store returns.
  • Fetched 0 products from the store / invalid identifiers listed: store-side cause, go to step 04 (iOS) or step 05 (Android).
  • No product request logged at all: dashboard-side cause, go to step 03.

Still Stuck? + Related Guides

0:02:00

Isolation tools: prove which layer is broken

  • Wait out propagation: new products (up to ~24h on Apple), new Play credentials (up to 36h), and fresh agreement signatures all take time. If you set things up today, the most effective fix is often tomorrow.
  • Network family errors (NetworkError, timeouts): check connectivity, disable VPN/proxy, confirm api.revenuecat.com isn't firewalled, and add retry-with-backoff for paywall loads. During App Store review specifically, design the paywall to degrade gracefully.
  • Test the dashboard chain without your app: call the REST API. If this returns your offering with products, the dashboard is fine and the issue is store-side or in your app:
    bash
    curl -s 'https://api.revenuecat.com/v1/subscribers/test_user/offerings' \
      -H 'Authorization: Bearer YOUR_PUBLIC_API_KEY' \
      -H 'X-Platform: ios'   # or android
  • Test your config without your code: RevenueCat's SampleCat sample app with your API key. If SampleCat fetches offerings and your app doesn't, the difference is in your app (initialization order, key, StoreKit config, bundle ID).
  • Test your code without the stores: RevenueCat's Test Store (iOS) / Test Store (Android) bypasses Apple/Google entirely. If offerings load with a Test Store key, your code and dashboard are fine, so the issue is store-side setup.
  • Stale cache after dashboard edits: offerings are cached for ~5 minutes; force-quit and relaunch after dashboard changes (and on Android, the Play Store app's own cache can lag; clearing Play Store data helps).

The complete checklist

  • ☐ One offering marked Current, with packages, each holding a product for your platform
  • ☐ Product IDs identical to the store (case, whitespace)
  • ☐ Correct public API key (appl_/goog_) from the correct project; bundle ID / applicationId matches
  • ☐ iOS: Paid Applications Agreement Active (banking + tax complete, not expired)
  • ☐ iOS: products have price, localization, review screenshot; subscriptions in a subscription group
  • ☐ iOS: at least one binary uploaded & processed; Simulator uses a StoreKit Configuration file (or device + sandbox account with it set to None)
  • ☐ Android: signed build on internal testing; tester added and opted in; installed once via Play
  • ☐ Android: products (and base plans) Active; Play credentials valid and > 36h old
  • ☐ Both: product/app availability covers your country; debug logs read end-to-end

If every box is ticked and offerings are still empty, gather your debug log and ask in the RevenueCat Community or contact support, and include the log lines around Error fetching offerings.

Related guides