Quickstart

Get up and running with mobile subscriptions

This guide will walk you through how to get up and running with subscriptions and RevenueCat's SDK with only a few lines of code.

1. Setting Up Your Account

We recommend using a company account when registering for RevenueCat and setting up your app within a project. You'll be able to invite the rest of your team as collaborators to your project, but only the project owner can manage billing. Note that there is no mechanism for transferring apps or projects between accounts, but you can change the account owner's email address. If you intend on selling an app, you should create a separate account.

2. Create a Project

Navigate to the RevenueCat dashboard and add a new project from the dropdown in the top navigation menu called Projects.

838838

3. Register Your App

From Project Settings > Apps in the left menu of the project dashboard, select the platform for the app you will be adding.

17321732

Project dashboard for selecting the app platform

The field App name is required to add your app to RevenueCat. The rest of the configuration fields can be added later.

12541254

App configuration page

After registering your app, we recommend setting up Platform Server Notifications. These notifications aren't required but will speed up webhooks and integration delivery times and reduce lag time updating your subscribers.

📘

Staging vs. Production apps

RevenueCat itself does not have separate environments for staging and production. Any RevenueCat app can make both sandbox and production purchases from the stores. If you do have separate apps for staging and production, you can create multiple projects in RevenueCat to mirror your setup.

4. Store Setup

Before you can start using RevenueCat to fetch products, you must configure your products in the respective stores. See the following guides for App Store Connect, Google Play Console, Amazon Appstore, and Stripe for help navigating through this process.

Service credentials need to be set up for RevenueCat to communicate with the app stores on your behalf. See our guides App Store Connect Shared Secret, Play Service Credentials, and Amazon Appstore Shared Secret for more information. Note that Play service credentials can take up to 36 hours to propagate throughout Google's servers.

If you are selling iOS products, be sure to sign your 'Paid Applications Agreement' and fill out your bank and tax information in App Store Connect > Agreements, Tax, and Banking. This needs to be completed before you can test any purchases.

5. Configure Your Products

Once your in-app products have been configured in App Store Connect, Google Play Console, Amazon Appstore, or Stripe, you'll need to copy that configuration into the RevenueCat dashboard. RevenueCat uses an Entitlements system to control access to premium features, and Offerings to manage the set of products you offer to customers.

Entitlements are the level of access that a customer is "entitled" to after purchasing a specific product, and Offerings is a simple way for you to organize the in-app products you wish to "offer" on your paywall and configure them remotely. We recommend utilizing these features to simplify your code and enable you to change products without releasing an app update.

See Configuring Products to set up your products and then organize them into Offerings or Entitlements.

11541154

6. Install the SDK

Our SDK seamlessly implements purchases and subscriptions across platforms while syncing tokens with the RevenueCat server.

If you run into issues with the SDK, see Troubleshooting the SDKs for guidance.

7. Configure the SDK

📘

Only use your public SDK key to configure Purchases

You can get your public SDK key from the API keys tab under Project settings in the dashboard.

You should only configure the shared instance of Purchases once, usually on app launch. After that, the same instance is shared throughout your app by accessing the .shared instance in the SDK.

See our guide on Configuring SDK for more information and best practices.

Make sure you configure Purchases with your public SDK key only. You can read more about the different API keys available in our Authentication guide.

// on iOS and tvOS, use `application:didFinishLaunchingWithOptions:`
// on macOS and watchOS use `applicationDidFinishLaunching:` 

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
  
    Purchases.logLevel = .debug
    Purchases.configure(withAPIKey: "public_sdk_key")
    return true
}
// on iOS and tvOS, use `application:didFinishLaunchingWithOptions:`
// on macOS and watchOS use `applicationDidFinishLaunching:` 

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    
    RCPurchases.logLevel = RCLogLevelDebug;
    [RCPurchases configureWithAPIKey:@"public_sdk_key"];
    
    return YES;
}
// If you're targeting only Google Play Store
class MainApplication: Application() {
    override fun onCreate() {
        super.onCreate()
        Purchases.debugLogsEnabled = true
        Purchases.configure(PurchasesConfiguration.Builder(this, "public_google_sdk_key").build())
    }
}

// If you're building for the Amazon Appstore, you can use flavors to determine which keys to use
// In your build.gradle:
flavorDimensions "store"
productFlavors {
    amazon {
        buildConfigField "String", "STORE", "\"amazon\""
    }

    google {
        buildConfigField "String", "STORE", "\"google\""
    }       
}

///...

class MainApplication: Application() {
    override fun onCreate() {
        super.onCreate()
        Purchases.debugLogsEnabled = true
          
        if (BuildConfig.STORE.equals("amazon")) {
            Purchases.configure(AmazonConfiguration.Builder(this, "public_amazon_sdk_key").build())
        } else if (BuildConfig.STORE.equals("google")) {
            Purchases.configure(PurchasesConfiguration.Builder(this, "public_google_sdk_key").build())
        }
    }
}
// If you're targeting only Google Play Store
public class MainApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        Purchases.setDebugLogsEnabled(true);
        Purchases.configure(new PurchasesConfiguration.Builder(this, "public_google_sdk_key").build());
    }
}

// If you're building for the Amazon Appstore, 
// click the Kotlin tab to see how to set up flavors in your build.gradle:
///...

public class MainApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        Purchases.setDebugLogsEnabled(true);
      
        PurchasesConfiguration.Builder builder = null;
      
        if (BuildConfig.STORE.equals("amazon")) {
            builder = new AmazonConfiguration.Builder(this, "public_amazon_sdk_key");
        } else if (BuildConfig.STORE.equals("google")) {
            builder = new PurchasesConfiguration.Builder(this, "public_google_sdk_key");
        }
      
        Purchases.configure(builder.build());
    }
}
import 'dart:io' show Platform;

//...

Future<void> initPlatformState() async {
  await Purchases.setDebugLogsEnabled(true);
  
  PurchasesConfiguration configuration;
  if (Platform.isAndroid) {
    configuration = PurchasesConfiguration("public_google_sdk_key");
    if (buildingForAmazon) { 
      // use your preferred way to determine if this build is for Amazon store
      // checkout our MagicWeather sample for a suggestion
      configuration = AmazonConfiguration("public_amazon_sdk_key");
    }
  } else if (Platform.isIOS) {
    configuration = PurchasesConfiguration("public_ios_sdk_key");
  }
  await Purchases.configure(configuration); Purchases.setup("public_amazon_sdk_key", useAmazon: true);
}
import { Platform } from 'react-native';

//...

export default class App extends React.Component {
 
  componentDidMount() {
    Purchases.setDebugLogsEnabled(true);
    
    if (Platform.OS === 'ios') {
        await Purchases.setup("public_ios_sdk_key");
    } else if (Platform.OS === 'android') {
        await Purchases.setup("public_google_sdk_key");
      
      // OR: if building for Amazon, be sure to follow the installation instructions then:
        await Purchases.setup({ apiKey: "public_amazon_sdk_key", useAmazon: true });
    }
    
  }
}
document.addEventListener("deviceready", onDeviceReady, false);

function onDeviceReady() {
    Purchases.setDebugLogsEnabled(true);
    if (window.cordova.platformId === 'ios') {
        Purchases.setup("public_ios_sdk_key");
    } else if (window.cordova.platformId === 'android') {
        Purchases.setup("public_google_sdk_key");
    }
    // OR: if building for Amazon, be sure to follow the installation instructions then:
    await Purchases.setup({ apiKey: "public_amazon_sdk_key", useAmazon: true });
}
See Unity installation instructions https://docs.revenuecat.com/docs/unity

When in development, we recommend enabling more verbose debug logs. For more information about these logs, see our Debugging guide.

If you're planning to use RevenueCat alongside your existing purchase code, check out our guide on Observer Mode.

📘

Configuring Purchases with User IDs

If you have a user authentication system in your app, you can provide a user identifier at the time of configuration or at a later date with a call to .logIn(). To learn more, check out our guide on Identifying Users.

8. Displaying Available Products

The SDK will automatically fetch the configured Offerings and retrieve the product information from Apple, Google, or Amazon. Thus, available products will already be loaded when customers launch your purchase screen.

Below is an example of fetching Offerings. You can utilize Offerings to organize your paywall screen. See our guide on Displaying Products for more information and best practices.

Purchases.shared.getOfferings { (offerings, error) in
    if let offerings = offerings {
      // Display current offering with offerings.current
  }
}
[[RCPurchases sharedPurchases] getOfferingsWithCompletion:^(RCOfferings *offerings, NSError *error) {
  if (offerings) {
        // Display current offering with offerings.current
  } else if (error) {
    // optional error handling
  }
}];
Purchases.sharedInstance.getOfferingsWith(
    onError = { error ->
    /* Optional error handling */ 
  },
  onSuccess = { offerings ->  
    // Display current offering with offerings.current
    }
}
Purchases.getSharedInstance().getOfferings(new ReceiveOfferingsCallback() {

     @Override
     public void onReceived(@NonNull Offerings offerings) {
     }

     @Override
     public void onError(@NonNull PurchasesError error) {
        /* Optional error handling */
     }
});
try {
  Offerings offerings = await Purchases.getOfferings();
  if (offerings.current != null) {
    // Display current offering with offerings.current
  }
} on PlatformException catch (e) {
    // optional error handling
}
try {
  const offerings = await Purchases.getOfferings();
  if (offerings.current !== null) {  
      // Display current offering with offerings.current
  }
} catch (e) {

}
func displayUpsellScreen() {
  Purchases.getOfferings(
      offerings => {
        if (offerings.current !== null) {  
          // Display current offering with offerings.current
        }
      },
      error => {

      }
  );
}
var purchases = GetComponent<Purchases>();
purchases.GetOfferings((offerings, error) =>
{
  if (error != null) {
    LogError(error);
  } else if (offerings.Current != null {
    // Display current offering with offerings.current
  }
});
curl --request GET \
  --url https://api.revenuecat.com/v1/subscribers/<app_user_id>/offerings \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer REVENUECAT_API_KEY' \
  --header 'Content-Type: application/json' \
  --header 'X-Platform: stripe'

See it in action

Swift Kotlin Flutter React Native

If fetching your Offerings, products, or available packages are empty, it's due to some configuration issue in the respective store.

The most common reasons for this in App Store Connect are an out-of-date 'Paid Applications Agreement' or products not at least in the 'Ready To Submit' state. In Google Play this usually occurs when the app is not published on a closed track and a valid test user added.

You can find more info about troubleshooting this issue in our Help Center.

9. Make a Purchase

The SDK includes a simple method for facilitating purchases. The purchase:package takes a package from the fetched Offering and processes the transaction with the respective app store.

The code sample below shows the process of purchasing a package and confirming it unlocks the "your_entitlement_id" content. More detail about the purchase:package method can be found in our guide on Making Purchases.

Purchases.shared.purchase(package: package) { (transaction, customerInfo, error, userCancelled) in
    if customerInfo?.entitlements.all["your_entitlement_id"]?.isActive == true {
        // Unlock that great "pro" content
    }
}
[[RCPurchases sharedPurchases] purchasePackage:package withCompletion:^(RCStoreTransaction *transaction, RCCustomerInfo *customerInfo, NSError *error, BOOL cancelled) {
    if (customerInfo.entitlements.all[@"your_entitlement_id"].isActive) {
    // User is "premium"
    }
}];
Purchases.sharedInstance.purchasePackageWith(
  this,
  package,
  onError = { error, userCancelled -> /* No purchase */ },
  onSuccess = { product, customerInfo ->
    if (customerInfo.entitlements["my_entitlement_identifier"]?.isActive == true) {
    // Unlock that great "pro" content
  }
})
Purchases.getSharedInstance().purchasePackage(this, packageToPurchase, new PurchaseCallback() {
    @Override
    public void onCompleted(@NonNull StoreTransaction storeTransaction, @NonNull CustomerInfo customerInfo) {
        if (customerInfo.getEntitlements().get("my_entitlement_identifier").isActive()) {
            // Unlock that great "pro" content
        }
    }

    @Override
    public void onError(@NonNull PurchasesError purchasesError, boolean b) {
        // No purchase
    }
});
try {
  PurchaserInfo purchaserInfo = await Purchases.purchasePackage(package);
  var isPro = purchaserInfo.entitlements.all["my_entitlement_identifier"].isActive;
  if (isPro) {
    // Unlock that great "pro" content
  }
} on PlatformException catch (e) {
  var errorCode = PurchasesErrorHelper.getErrorCode(e);
  if (errorCode != PurchasesErrorCode.purchaseCancelledError) {
    showError(e);             
  }
}
// Using packages
try {
  const purchaseMade = await Purchases.purchasePackage(package);
  if (typeof purchaseMade.purchaserInfo.entitlements.active.my_entitlement_identifier !== "undefined") {
    // Unlock that great "pro" content
  }
} catch (e) {
  if (!e.userCancelled) {
    showError(e);
  }
}

// Note: if you are using purchaseProduct to purchase Android In-app products, an optional third parameter needs to be provided when calling purchaseProduct. You can use the package system to avoid this
await Purchases.purchaseProduct("product_id", null, Purchases.PURCHASE_TYPE.INAPP);
Purchases.purchasePackage(package, ({ productIdentifier, purchaserInfo }) => {
    if (typeof purchaserInfo.entitlements.active.my_entitlement_identifier !== "undefined") {
      // Unlock that great "pro" content
    }
  },
  ({error, userCancelled}) => {
    // Error making purchase
  }
);

// Note: if you are using purchaseProduct to purchase Android In-app products, an optional third parameter needs to be provided when calling purchaseProduct. You can use the package system to avoid this.

Purchases.purchaseProduct("product_id", ({ productIdentifier, purchaserInfo }) => {
}, ({error, userCancelled}) => {
    // Error making purchase
}, null, Purchases.PURCHASE_TYPE.INAPP);
Purchases purchases = GetComponent<Purchases>();
purchases.PurchasePackage(package, (productIdentifier, purchaserInfo, userCancelled, error) =>
{
  if (purchaserInfo.Entitlements.Active.ContainsKey("my_entitlement_identifier")) {
    // Unlock that great "pro" content
  }
});
curl -X POST \
  https://api.revenuecat.com/v1/receipts \
  -H 'Content-Type: application/json' \
  -H 'X-Platform: stripe' \
  -H 'Authorization: Bearer YOUR_REVENUECAT_API_KEY' \
  -d '{ "app_user_id": "my_app_user_id",
  "fetch_token": "sub_xxxxxxxxxx"
  }'

See it in action

Swift Kotlin Flutter React Native

📘

purchase:package handles the underlying framework interaction and automatically validates purchases with the respective store through our secure servers. This helps reduce in-app purchase fraud and decreases the complexity of your app. Receipt tokens are stored remotely and always kept up to date by RevenueCat.

10. Get Subscription Status

The SDK makes it easy to check what active subscriptions the current customer has, too. This can be done by checking if a specific Entitlement is active, or by checking if the active Entitlements array contains a specific Entitlement ID.

If you're not using Entitlements (you probably should be!) you can check the array of active subscriptions to see what product IDs from the respective store it contains.

Purchases.shared.getCustomerInfo { (customerInfo, error) in
    if customerInfo?.entitlements.all["your_entitlement_id"]?.isActive == true {
        // User is "premium"
    }
}
[[RCPurchases sharedPurchases] getCustomerInfoWithCompletion:^(RCPurchaserInfo * customerInfo, NSError * error) {
    if (customerInfo.entitlements.all[@"your_entitlement_id"].isActive) {
    // User is "premium"
    }
}];
Purchases.sharedInstance.getCustomerInfo({ error -> /* Optional error handling */ }) { customerInfo ->
  if (customerInfo.entitlements["my_entitlement_identifier"]?.isActive == true) {
    // Grant user "pro" access
  }
}
Purchases.getSharedInstance().getCustomerInfo(new ReceiveCustomerInfoCallback() {
    @Override
    public void onReceived(@NonNull CustomerInfo customerInfo) {
        if (customerInfo.getEntitlements().get("my_entitlement_identifier").isActive()) {
            // Grant user "pro" access
        }
    }

    @Override
    public void onError(@NonNull PurchasesError purchasesError) {
    }
});
try {
  PurchaserInfo purchaserInfo = await Purchases.getPurchaserInfo();
  if (purchaserInfo.entitlements.all["my_entitlement_identifier"].isActive) {
    // Grant user "pro" access
  }
} on PlatformException catch (e) {
  // Error fetching purchaser info
}
try {
  const purchaserInfo = await Purchases.getPurchaserInfo();
  if(typeof purchaserInfo.entitlements.active.my_entitlement_identifier !== "undefined") {
    // Grant user "pro" access
  }
} catch (e) {
 // Error fetching purchaser info
}
Purchases.getPurchaserInfo(
  info => {
    const isPro = typeof purchaserInfo.entitlements.active.my_entitlement_identifier !== "undefined";
  },
  error => {
    // Error fetching purchaser info
  }
);
var purchases = GetComponent<Purchases>();
purchases.GetPurchaserInfo((info, error) =>
{
   if (purchaserInfo.Entitlements.Active.ContainsKey("my_entitlement_identifier")) {
    // Unlock that great "pro" content
  }
});
curl --request GET \
  --url https://api.revenuecat.com/v1/subscribers/<app_user_id> \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer REVENUECAT_API_KEY' \
  --header 'Content-Type: application/json'

See it in action

Swift Kotlin Flutter React Native

You can use this method whenever you need to get the latest status, and it's safe to call this repeatedly throughout the lifecycle of your app. Purchases automatically caches the latest CustomerInfo whenever it updates — so in most cases, this method pulls from the cache and runs very fast.

It's typical to call this method when deciding which UI to show the user and whenever the user performs an action that requires a certain entitlement level.

📘

You can access a lot more information about a subscription than simply whether it's active or not. See our guide on Subscription Status to learn if subscription is set to renew, if there's an issue detected with the user's credit card, and more.

11. Restoring Purchases

RevenueCat enables your users to restore their in-app purchases, reactivating any content that they previously purchased from the same store account (Apple, Google, or Amazon account). We recommend that all apps have some way for users to trigger the restore method. Note that Apple does require a restore mechanism in the event a user loses access to their purchases (e.g: uninstalling/reinstalling the app, losing their account information, etc).

Purchases.shared.restorePurchases { (customerInfo, error) in
    //... check customerInfo to see if entitlement is now active
}
[[RCPurchases sharedPurchases] restorePurchasesWithCompletion:^(RCCustomerInfo *customerInfo, NSError *error) {
    //... check customerInfo to see if entitlement is now active
}];
Purchases.sharedInstance.restorePurchasesWith(::showError) { customerInfo ->
    //... check customerInfo to see if entitlement is now active
}
Purchases.getSharedInstance().restorePurchases(new ReceiveCustomerInfoCallback() {
    @Override
    public void onReceived(@NonNull CustomerInfo customerInfo) {
        //... check purchaserInfo to see if entitlement is now active   
    }

    @Override
    public void onError(@NonNull PurchasesError purchasesError) {

    }
});
try {
  PurchaserInfo restoredInfo = await Purchases.restoreTransactions();
  // ... check restored purchaserInfo to see if entitlement is now active
} on PlatformException catch (e) {
  // Error restoring purchases
}
try {
  const restore = await Purchases.restoreTransactions();
  // ... check restored purchaserInfo to see if entitlement is now active
} catch (e) {

}
Purchases.restoreTransactions(
  info => {
    //... check purchaserInfo to see if entitlement is now active
  },
  error => {
    // Error restoring purchases
  }
);
var purchases = GetComponent<Purchases>();
purchases.RestoreTransactions((info, error) =>
{
    //... check purchaserInfo to see if entitlement is now active
});

See it in action

Swift Kotlin Flutter React Native

If two different App User IDs restore transactions from the same underlying store account (Apple, Google, or Amazon account) RevenueCat may attempt to create an alias between the two App User IDs and count them as the same user going forward. See our guide on Restoring Purchases for more information on the different configurable restore behaviors.

(optional) Listening For CustomerInfo Updates

Since the SDK works seamlessly on any platform, changes to a user's purchase info may come from a variety of sources. You can respond to any changes in a customer's CustomerInfo by conforming to an optional delegate method, purchases:receivedUpdated:. This will fire whenever the SDK receives an updated customer info object from calls to getCustomerInfo(), purchase(package:), purchase(product:), or restorePurchases().

CustomerInfo updates are not pushed to your app from the RevenueCat backend, updates can only happen from an outbound network request to RevenueCat.

Depending on your app, it may be sufficient to ignore the delegate and simply handle changes to customer information the next time your app is launched or in the completion blocks of the SDK methods.

// Additional configure setup
// on iOS and tvOS, use `application:didFinishLaunchingWithOptions:`
// on macOS and watchOS use `applicationDidFinishLaunching:` 

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
  
    Purchases.logLevel = .debug
    Purchases.configure(withAPIKey: "public_sdk_key")
    Purchases.shared.delegate = self // make sure to set this after calling configure
    
    return true
}

extension AppDelegate: PurchasesDelegate {
    func purchases(_ purchases: Purchases, receivedUpdated customerInfo: CustomerInfo) {
        /// - handle any changes to the user's CustomerInfo
    }
}
// Additional configure setup
// on iOS and tvOS, use `application:didFinishLaunchingWithOptions:`
// on macOS and watchOS use `applicationDidFinishLaunching:`

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    RCPurchases.logLevel = RCLogLevelDebug;
    [RCPurchases configureWithAPIKey:@"public_sdk_key"];
    RCPurchases.sharedPurchases.delegate = self;

    return YES;
}

- (void)purchases:(nonnull RCPurchases *)purchases receivedUpdatedCustomerInfo:(nonnull RCCustomerInfo *)customerInfo {
    // handle any changes to purchaserInfo
}
class UpsellActivity : AppCompatActivity(), UpdatedCustomerInfoListener {
        override fun onReceived(customerInfo: CustomerInfo) {
        // handle any changes to purchaserInfo
    }
}
public class UpsellActivity extends AppCompatActivity implements UpdatedCustomerInfoListener {
        @Override
    public void onReceived(CustomerInfo customerInfo) {
        // handle any changes to customerInfo
    }
}
Purchases.addPurchaserInfoUpdateListener((purchaserInfo) => {
  // handle any changes to purchaserInfo
});
Purchases.addPurchaserInfoUpdateListener(info => {
    // handle any changes to purchaserInfo
});
// subscribe to the window event onPurchaserInfoUpdated to get any changes that happen in the purchaserInfo
window.addEventListener("onPurchaserInfoUpdated", onPurchaserInfoUpdated, false);

//...

onPurchaserInfoUpdated: function(info) {
    // handle any changes to purchaserInfo
}
public override void PurchaserInfoReceived(Purchases.PurchaserInfo purchaserInfo)
{
    // handle any changes to purchaserInfo
}

See it in action

Swift Kotlin Flutter React Native

👍

You did it!

You have now implemented a fully-featured subscription purchasing system without spending a month writing server code. Congrats!

Sample Apps

To download more complete examples of integrating the SDK, head over to our sample app resources.

View Samples

Next Steps


Did this page help you?