RevenueCat

Build With RevenueCat

Build a customized mobile subscription business with RevenueCat. We do the heavy lifting of normalizing subscribers from any source and maintain a single source of truth for subscription status, so you can get back to building your app.

RevenueCat is a powerful, secure, reliable, and free to use in-app purchase server with global support. All you need to get started is an API key.

Get Started    API Reference

Displaying Products

Build dynamic paywalls and control available products remotely

If you've configured Offerings in RevenueCat, you can control which products are shown to users without requiring an app update. Building paywalls that are dynamic and can react to different product configurations gives you maximum flexibility to make remote updates.

📘

Before products and offerings can be fetched from RevenueCat, be sure to initialize the Purchases SDK by following our Quickstart guide.

Fetching Offerings

Offerings are fetched through the Purchases SDK based on their configuration in the RevenueCat dashboard.

The getOfferings method will fetch the Offerings from RevenueCat. These are pre-fetched in most cases on app launch, so the completion block to get offerings won't need to make a network request in most cases.

Purchases.shared.offerings { (offerings, error) in
    if let packages = offerings?.current?.availablePackages {
        // Display packages for sale
    }
}
[[RCPurchases sharedPurchases] offeringsWithCompletionBlock:^(RCOfferings *offerings, NSError *error) {
  if (offerings.current && offerings.current.availablePackages.count != 0) {
    // Display packages for sale
  } else if (error) {
    // optional error handling
  }
}];
Purchases.sharedInstance.getOfferingsWith({ error ->
  // An error occurred
}) { offerings ->
  offerings.current?.availablePackages?.takeUnless { it.isNullOrEmpty() }?.let {
    // Display packages for sale
  }
}
Purchases.getSharedInstance().getOfferings(new ReceiveOfferingsListener() {
  @Override
  public void onReceived(@NonNull Offerings offerings) {
    if (offerings.getCurrent() != null) {
      List<Package> availablePackages = offerings.getCurrent().getAvailablePackages();
      // Display packages for sale
    }
  }
  
  @Override
  public void onError(@NonNull PurchasesError error) {
    // An error occurred
  }
});
try {
  Offerings offerings = await Purchases.getOfferings();
  if (offerings.current != null && offerings.current.availablePackages.isNotEmpty) {
    // Display packages for sale
  }
} on PlatformException catch (e) {
    // optional error handling
}
try {
  const offerings = await Purchases.getOfferings();
  if (offerings.current !== null && offerings.current.availablePackages.length !== 0) {
    // Display packages for sale
  }
} catch (e) {
 
}
func displayUpsellScreen() {
  Purchases.getOfferings(
      offerings => {
        if (offerings.current !== null && offerings.current.availablePackages.length !== 0) {  
                // Display packages for sale
        }
      },
      error => {

      }
  );
}
var purchases = GetComponent<Purchases>();
purchases.GetOfferings((offerings, error) =>
{
  if (offerings.Current != null && offerings.Current.AvailablePackages.Count != 0){
    // Display packages for sale
  }
});

You must choose one Offering that is the "Current Offering" - which can easily be accessed via the current property of the returned offerings.

To change the current Offering, navigate to the Offerings tab for your app in the RevenueCat dashboard and click Make current next to the Offering you'd like to enable.

Offerings can be updated at any time, and the changes will go into effect for all users right away.

Custom Offering identifiers

It's also possible to access other Offerings besides the "Current Offering" directly by it's identifier.

Purchases.shared.offerings { (offerings, error) in
    if let packages = offerings?.offering(identifier: "experiment_group").availablePackages {
        // Display packages for sale
    }
}
[[RCPurchases sharedPurchases] offeringsWithCompletionBlock:^(RCOfferings *offerings, NSError *error) {
    NSArray<RCPackage *> *availablePackages = [offerings offeringWithIdentifier:"experiment_group"].availablePackages;
  if (availablePackages) {
    // Display packages for sale
  }
}];
Purchases.sharedInstance.getOfferingsWith({ error ->
  // An error occurred
}) { offerings ->
  offerings["experiment_group"]?.availablePackages?.takeUnless { it.isNullOrEmpty() }?.let {
    // Display packages for sale
  }
}
Purchases.getSharedInstance().getOfferings(new ReceiveOfferingsListener() {
  @Override
  public void onReceived(@NonNull Offerings offerings) {
    if (offerings.get("experiment_group") != null) {
      List<Package> availablePackages = offerings.get("experiment_group").getAvailablePackages();
      // Display packages for sale
    }
  }
  
  @Override
  public void onError(@NonNull PurchasesError error) {
    // An error occurred
  }
});
try {
  Offerings offerings = await Purchases.getOfferings();
  if (offerings.getOffering("experiment_group").availablePackages.isNotEmpty) {
    // Display packages for sale
  }
} on PlatformException catch (e) {
    // optional error handling
}
try {
  const offerings = await Purchases.getOfferings();
  if (offerings.all["experiment_group"].availablePackages.length !== 0) {
    // Display packages for sale
  }
} catch (e) {
 
}
Purchases.getOfferings(
      offerings => {
        if (offerings.all["experiment_group"].availablePackages.length !== 0) {
                // Display packages for sale
        }
      },
      error => {

      }
  );
var purchases = GetComponent<Purchases>();
purchases.GetOfferings((offerings, error) =>
{
  if (offerings.All.ContainsKey("experiment_group") && offerings.All["experiment_group"].AvailablePackages.Count != 0) {
    // Display packages for sale
  }
});

Displaying Packages

Packages help abstract platform-specific products by grouping equivalent products on iOS and Android. A package is made up of three parts: identifier, type, and underlying store product.

Name

Description

Identifier

The package identifier (e.g. com.revenuecat.app.monthly)

Type

The type of the package:

  • UNKNOWN
  • CUSTOM
  • LIFETIME
  • ANNUAL
  • SIX_MONTH
  • THREE_MONTH
  • TWO_MONTH
  • MONTHLY
  • WEEKLY

Product

The underlying product that is mapped to this package. Either an SKProduct (iOS) or SkuDetails (Android).

Choosing which Offering to display

In practice, you may not want to display the default current Offering to every user and instead have a specific cohort that see a different Offering.

For example, displaying a higher priced Offering to users that came from paid acquisition to help recover ad costs, or a specific Offering designed to show iOS Subscription Offers when a user has cancelled their subscription.

This can be accomplished with custom Offering identifiers for each of these "cohorts".

Purchases.shared.offerings { (offerings, error) in
    var packages : [Package]?
    
    if user.isPaidDownload {
        packages = offerings?.offering(identifier: "paid_download_offer")?.availablePackages
    } else if user.signedUpOver30DaysAgo {
        packages = offerings?.offering(identifier: "long_term_offer")?.availablePackages
    } else if user.recentlyChurned {
        packages = offerings?.offering(identifier: "ios_subscription_offer")?.availablePackages
    }
    
    // Present your paywall
}
[[RCPurchases sharedPurchases] offeringsWithCompletionBlock:^(RCOfferings *offerings, NSError *error) {
  NSArray<RCPackage *> *packages;
  
  if (user.isPaidDownload) {
    packages = [offerings offeringWithIdentifier:"paid_download_offer"].availablePackages;
  } else if (user.signedUpOver30DaysAgo) {
    packages = [offerings offeringWithIdentifier:"long_term_offer"].availablePackages;
  } else if (user.recentlyChurned) {
    packages = [offerings offeringWithIdentifier:"ios_subscription_offer"].availablePackages;
  }
  
  [self presentPaywallWithPackages:packages];
}];
Purchases.sharedInstance.getOfferingsWith({ error ->
  // An error occurred
}) { offerings ->
  val packages: Package? = when {
    user.isPaidDownload -> offerings["paid_download_offer"]?.availablePackages
    user.signedUpOver30DaysAgo -> offerings["long_term_offer"]?.availablePackages
    user.recentlyChurned -> offerings["ios_subscription_offer"].availablePackages
    else -> null
  }
    presentPaywall(packages)
}
Purchases.getSharedInstance().getOfferings(new ReceiveOfferingsListener() {
  @Override
  public void onReceived(@NonNull Offerings offerings) {
    List<Package> packages = null;
    if (user.isPaidDownload) {
      if (offerings.get("paid_download_offer") != null) {
        packages = offerings.get("paid_download_offer").getAvailablePackages();
      }
    } else if (user.signedUpOver30DaysAgo) {
      if (offerings.get("long_term_offer") != null) {
        packages = offerings.get("long_term_offer").getAvailablePackages();
      }
    }
    presentPaywall(packages);
  }
  
  @Override
  public void onError(@NonNull PurchasesError error) {
    // An error occurred
  }
});
try {
  Offerings offerings = await Purchases.getOfferings();
  var packages;
  if (user.isPaidDownload) {
    packages = offerings?.getOffering("paid_download_offer")?.availablePackages;
  } else if (user.signedUpOver30DaysAgo) {
    packages = offerings?.getOffering("long_term_offer")?.availablePackages;
  } else if (user.recentlyChurned) {
    packages = offerings?.getOffering("ios_subscription_offer")?.availablePackages;
  }
  presentPaywall(packages);
} on PlatformException catch (e) {
    // optional error handling
}
try {
  const offerings = await Purchases.getOfferings();
  let packages;
  if (user.isPaidDownload) {
    packages = offerings.all["paid_download_offer"].availablePackages;
  } else if (user.signedUpOver30DaysAgo) {
    packages = offerings.all["long_term_offer"].availablePackages;
  } else if (user.recentlyChurned) {
    packages = offerings.all["ios_subscription_offer"].availablePackages;
  }
  presentPaywall(packages);
} catch (e) {
 
}
Purchases.getOfferings(
      offerings => {
        let packages;
        if (user.isPaidDownload) {
          packages = offerings.all["paid_download_offer"].availablePackages;
        } else if (user.signedUpOver30DaysAgo) {
          packages = offerings.all["long_term_offer"].availablePackages;
        } else if (user.recentlyChurned) {
          packages = offerings.all["ios_subscription_offer"].availablePackages;
        }
        presentPaywall(packages);
      },
      error => {

      }
  );
var purchases = GetComponent<Purchases>();
purchases.GetOfferings((offerings, error) =>
{
  List<Purchases.Package> packages;
  if (user.isPaidDownload) {
    packages = offerings.All["paid_download_offer"].AvailablePackages;
  } else if (user.signedUpOver30DaysAgo) {
    packages = offerings.All["long_term_offer"].AvailablePackages;
  } else if (user.recentlyChurned) {
    packages = offerings.All["ios_subscription_offer"].AvailablePackages;
  }
  presentPaywall(packages);
});

📘

As of now, cohort logic needs to be managed outside of RevenueCat.

Best Practices

Do

Don't

✅Make paywalls dynamic by minimizing or eliminating any hardcoded strings

❌Make static paywalls hardcoded with specific product IDs

✅Use default package types

❌Use custom package identifiers in place of a default option

✅Allow for any number of product choices

❌Support only a fixed number of products

✅Support for different free trial durations, or no free trial

❌Hardcode free trial text

Next Steps

  • Now that you've shown the correct products to users, it's time to make a purchase
  • Check out our sample apps for examples of how to display products.

Updated 20 days ago


Displaying Products


Build dynamic paywalls and control available products remotely

Suggested Edits are limited on API Reference Pages

You can only suggest edits to Markdown body content, but not to the API spec.