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.

Quickstart

Get up and running with mobile subscriptions

This guide will walk you through a practical example to get you up and running with subscriptions and the Purchases SDK with only a few lines of code.

Register your app in RevenueCat

Navigate to the the RevenueCat dashboard and add a new app from the dropdown.

One app for all platforms

A single app in RevenueCat supports all platforms.

Setup your Entitlements

Entitlements are a simple way for you to organize your in-app products and configure them remotely. This simplifies the client side code and enables you to change products without an app update.

See Understanding Entitlements for how to configure and leverage entitlements.

Configure Products First

Before configuring entitlements, you should setup your in-app products in either App Store Connect or Play Developer Console.

A common and simple setup example is one entitlement with identifier pro, one offering monthly, with one product.

Install the Purchases SDK

Purchases is our SDK that correctly implements purchases and subscriptions across platforms while syncing tokens with the RevenueCat server.

Configure Purchases

You can get your API key from app settings in the dashboard.

You should only configure Purchases once (usually on app launch) as soon as your app has a unique user id for your user. This can be when a user logs in if you have accounts or on launch if you can generate a random user identifier. The same instance is shared throughout your app by accessing the .shared instance in the SDK.

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    
    Purchases.debugLogsEnabled = true
    Purchases.automaticAttributionCollection = true
    Purchases.configure(withAPIKey: "my_api_key", appUserID: "my_app_user_id")
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  	// Override point for customization after application launch.
    
  	RCPurchases.debugLogsEnabled = YES;
  	RCPurchases.automaticAttributionCollection = YES;
  	[RCPurchases configureWithAPIKey:@"my_api_key" appUserID:@"my_app_user_id"];
    
  	return YES;
}
class MainApplication: Application() {

    override fun onCreate() {
        super.onCreate()
        Purchases.debugLogsEnabled = true
        Purchases.configure(this, "my_api_key", "my_app_user_id")
    }

}

public class MainApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        Purchases.setDebugLogsEnabled(true);
        Purchases.configure(this, "my_api_key", "my_app_user_id");
    }

}
export default class App extends React.Component {
  componentWillMount() {
    Purchases.setDebugLogsEnabled(true);
    Purchases.setup("my_api_key", "my_app_user_id");
  }
}
constructor(public platform: Platform) {
  platform.ready().then(() => {
    Purchases.setDebugLogsEnabled(true);
    Purchases.setup(
      "my_api_key",
      "my_app_user_id"
    );
  });
}
See Unity installation instructions https://docs.revenuecat.com/docs/unity

Usage without User IDs

If your app doesn't have user IDs, you can use .configure(withAPIKey:) or pass nil for the appUserID. We will generate a random User ID for you.

Displaying Available Products

Purchases will automatically fetch the latest active entitlements and get the product information from Apple or Google. This means when users launch your purchase screen, products will already be loaded.

Below is an example of fetching entitlements and launching an upsell screen.

func displayUpsellScreen() {
    Purchases.shared.entitlements { (entitlements, error) in
        let vc = UpsellController()
        vc.entitlements = entitlements
        presentViewController(vc, animated: true, completion: nil)
    }
}
[[RCPurchases sharedPurchases] entitlementsWithCompletionBlock:^(RCEntitlements *entitlements, NSError *error) {
  UpsellViewController *vc = [[UpsellViewController alloc] init];
  vc.entitlements = entitlements;
  [self presentViewController:vc animated:YES completion:nil];
}];
Purchases.sharedInstance.getEntitlements(ReceiveEntitlementsListener { entitlements, error ->
    showUpsellScreen(entitlements)
})
  
// or
  
Purchases.sharedInstance.getEntitlementsWith(onError = { /* Optional error handling */ }) { entitlementMap ->  

}
Purchases.getSharedInstance().getEntitlements(new ReceiveEntitlementsListener() {
            @Override
    public void onReceived(@Nullable Map<String, Entitlement> entitlements, @Nullable PurchasesError error) {
        showScreen(entitlements);
    }
});
try {
  const entitlements = await Purchases.getEntitlements();
  showUpsellScreen(entitlements);
} catch (e) {

}
func displayUpsellScreen() {
  Purchases.getEntitlements(
      entitlements => {

      },
      error => {

      }
  );
}
var purchases = GetComponent<Purchases>();
purchases.GetProducts(new []{ "onemonth_freetrial", "annual_freetrial" }, (products, error) =>
{
    if (error != null) {
        LogError(error);
    }
});

Since the SDK pre-fetches the available products, the completion block to get entitlements won't need to make a network request in most cases. This creates a seamless onboarding experience that results in less drop-off, happier users, and more revenue for you.

Active products are nil

If your active products are returning nil, make sure they're configured correctly in the Play Store or App Store Connect.

Make a Purchase

When it comes time to make a purchase, Purchases has a simple method, makePurchase. The code sample below shows the process of purchasing a product and confirming it unlocks the "my_entitlement_identifier" content.

Purchases.shared.makePurchase(product, { (transaction, purchaserInfo, error, cancelled) in
    if let purchaserInfo = purchaserInfo {
    
        if purchaserInfo.activeEntitlements.contains("my_entitlement_identifier") {
            // Unlock that great "pro" content
        }
        
    }
})
[[RCPurchases sharedPurchases] makePurchase:product withCompletionBlock:^(SKPaymentTransaction *transaction, RCPurchaserInfo *purchaserInfo, NSError *error, BOOL cancelled) {
    if ([purchaserInfo.activeEntitlements containsObject:@"my_entitlement_identifier"]) {
        // Unlock that great "pro" content
    }
}];
Purchases.sharedInstance.makePurchaseWith(
    this,
    product.sku,
    BillingClient.SkuType.SUBS,
    { error, userCancelled ->
        // No purchase
    }) { product, purchaserInfo ->
        
        if (purchaserInfo.activeEntitlements.contains("my_entitlement_identifier")) {
            // Unlock that great "pro" content
        }
    }
Purchases.getSharedInstance().makePurchase(
    this,
    product.getSku(),
    BillingClient.SkuType.SUBS,
    new MakePurchaseListener() {
        @Override
        public void onCompleted(@NonNull Purchase purchase, @NonNull PurchaserInfo purchaserInfo) {
            if (purchaserInfo.getActiveEntitlements().contains("my_entitlement_identifier")) {
                // Unlock that great "pro" content
            }
        }

        @Override
        public void onError(@NonNull PurchasesError error, Boolean userCancelled) {
          // No purchase
        }
    }
);
try {
	const purchaseMade = await Purchases.makePurchase("product_id");
  if (purchaseMade.purchaserInfo.activeEntitlements !== "undefined" &&
      purchaseMade.purchaserInfo.activeEntitlements.includes("my_entitlement_identifier")) {
    // Unlock that great "pro" content
  }
} catch (e) {
	showError(e);
}


Purchases.makePurchase("product_id", 
  (productIdentifier, purchaserInfo) {
    if (purchaserInfo.activeEntitlements.includes("my_entitlement_identifier")) {
      // Unlock that great "pro" content
    }
  },
  error => {
    // Error making purchase
  }
})
Purchases purchases = GetComponent<Purchases>();
purchases.MakePurchase(product, (productIdentifier, purchaserInfo, userCancelled, error) =>
{
    foreach(string sub in info.ActiveSubscriptions)
    {
        if (item.Contains("my_product_identifier")) {
           // Unlock that great "pro" content
        }
    }
});

makePurchase handles the underlying framework interaction and automatically validates purchases with Apple or Google 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. Note that a physical device and sandbox account are required for testing purchases.

Get Subscription Status

Purchases makes it easy to check what active subscriptions the current user has. This can be done two ways within the .purchaserInfo method:

  • Checking active Entitlements - this lets you see what entitlements (from RevenueCat dashboard) are active for the user.
  • Checking the active subscriptions - this lets you see what product ids (from iTunes Connect or Play Store) are active for the user.

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 purchaserInfo whenever it updates so in most cases this method is pulling from the cache and is run very fast.

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

Purchases.shared.purchaserInfo { (purchaserInfo, error) in
    if let purchaserInfo = purchaserInfo {

        // Option 1: Check if user has access to entitlement (from RevenueCat dashboard)
        if purchaserInfo.activeEntitlements.contains("my_entitlement_identifier") {
            // Grant user "pro" access
        }

        // Option 2: Check if user has active subscription (from App Store Connect or Play Store)
        if purchaserInfo.activeSubscriptions.contains("my_product_identifier") {
            // Grant user "pro" access
        }
    }
}
[[RCPurchases sharedPurchases] purchaserInfoWithCompletionBlock:^(RCPurchaserInfo * purchaserInfo, NSError * error) {
        
    // Option 1: Check if user has access to entitlement (from RevenueCat dashboard)
    if ([purchaserInfo.activeEntitlements containsObject:@"my_entitlement_identifier"]) {
        // Grant user "pro" access
    }

    // Option 2: Check if user has active subscription (from App Store Connect or Play Store)
    if ([purchaserInfo.activeSubscriptions containsObject:@"my_product_identifier"]) {
        // Grant user "pro" access
    }
}];
Purchases.sharedInstance.getPurchaserInfo(ReceivePurchaserInfoListener { purchaserInfo, error ->
    if purchaserInfo?.let {
        // Option 1: Check if user has access to entitlement (from RevenueCat dashboard)
        if (it.activeEntitlements.contains("my_entitlement_identifier")) {
            // Grant user "pro" access
        }

        // Option 2: Check if user has active subscription (from App Store Connect or Play Store)
        if (it.activeSubscriptions.contains("my_product_identifier")) {
            // Grant user "pro" access
        }
    }
})
  
// or

Purchases.sharedInstance.getPurchaserInfoWith({ error -> /* Optional error handling */ }) { purchaserInfo ->
       // Option 1: Check if user has access to entitlement (from RevenueCat dashboard)
        if (it.activeEntitlements.contains("my_entitlement_identifier")) {
            // Grant user "pro" access
        }

        // Option 2: Check if user has active subscription (from App Store Connect or Play Store)
        if (it.activeSubscriptions.contains("my_product_identifier")) {
            // Grant user "pro" access
        }         
}
Purchases.getSharedInstance().getPurchaserInfo(new ReceivePurchaserInfoListener() {
    @Override
    public void onReceived(@android.support.annotation.Nullable PurchaserInfo purchaserInfo, @android.support.annotation.Nullable PurchasesError error) {
        if (purchaserInfo != null) {
            // Option 1: Check if user has access to entitlement (from RevenueCat dashboard)
            if (purchaserInfo.getActiveEntitlements().contains("my_entitlement_identifier")) {
                // Grant user "pro" access
            }

            // Option 2: Check if user has active subscription (from App Store Connect or Play Store)
            if (purchaserInfo.getActiveSubscriptions().contains("my_product_identifier")) {
                // Grant user "pro" access
            }
        }
    }
});
try {
  const purchaserInfo = await Purchases.getPurchaserInfo();
	// Option 1: Check if user has access to entitlement (from RevenueCat dashboard)
  if(purchaserInfo.activeEntitlements !== "undefined" && purchaserInfo.activeEntitlements.includes("my_entitlement_identifier")) {
    // Grant user "pro" access
  }
  // Option 2: Check if user has active subscription (from App Store Connect or Play Store)
  if (purchaserInfo.activeSubscriptions !== "undefined" && purchaserInfo.activeSubscriptions.includes("my_product_identifier")) {
    // Grant user "pro" access
  }
} catch (e) {
 // Error fetching purchaser info
}
Purchases.getPurchaserInfo(
  info => {
    // Option 1: Check if user has access to entitlement (from RevenueCat dashboard)
    const isPro =
      info.activeEntitlements !== "undefined" &&
      info.activeEntitlements.includes("pro");

    // Option 2: Check if user has active subscription (from App Store Connect or Play Store)
    const isPro =
      info.activeSubscriptions !== "undefined" &&
      info.activeSubscriptions.includes("my_product_identifier");
  },
  error => {
    // Error fetching purchaser info
  }
);
var purchases = GetComponent<Purchases>();
purchases.GetPurchaserInfo((info, error) =>
{
    foreach(string sub in info.ActiveSubscriptions)
    {
        if (item.Contains("my_product_identifier")) {
           // User has "pro" access
        }
    }
});

Since the SDK updates and caches the latest PurchaserInfo when the app becomes active, the completion block in .purchaserInfo won't need to make a network request in most cases.

Listening For Purchaser Info Updates

Since Purchases SDK works seamlessly on any platform, a user's purchase info may change from a variety of sources. You can respond to any changes in purchaser info by conforming to an optional delegate method, didReceivePurchaserInfo:. This will fire whenever we receive a change in purchaser info and you should expect it to be called at launch and throughout the life of the app.

Depending on your app, it may be sufficient to ignore the delegate and simply handle changes to purchaser information the next time your app is launched.

func purchases(_ purchases: Purchases, didReceiveUpdated purchaserInfo: PurchaserInfo) {
    // handle any changes to purchaserInfo
}
- (void)purchases:(nonnull RCPurchases *)purchases didReceiveUpdatedPurchaserInfo:(nonnull RCPurchaserInfo *)purchaserInfo {
    // handle any changes to purchaserInfo
}
class UpsellActivity : AppCompatActivity(), UpdatedPurchaserInfoListener {
		override fun onReceived(purchaserInfo: PurchaserInfo?) {
        // handle any changes to purchaserInfo
    }
}
public class UpsellActivity extends AppCompatActivity implements UpdatedPurchaserInfoListener {
		@Override
    public void onReceived(PurchaserInfo 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
}

You've done it!

That's all there is to it! You have now implemented a fully featured subscription purchasing system without spending a month writing server code. Congrats!

Sample Apps

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

View Samples

Next Steps

Quickstart


Get up and running with mobile subscriptions

Suggested Edits are limited on API Reference Pages

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