Skip to content
Blog

In-App Purchases: RevenueCat and StoreKit 2

Learn how to add in-app purchases and subscriptions to your mobile app with RevenueCat, StoreKit 2, and Billing Client 7 — entitlements, paywalls, and webhooks.

Published on August 18, 2026

AI Assistant

Monetizing a mobile app means dealing with two storefronts that work completely differently: Apple’s StoreKit on iOS and Google’s Play Billing on Android. Products, receipt validation, refunds, and storefront taxes all differ. RevenueCat is the middleware that normalizes this — one SDK, one API, and a dashboard that tracks revenue, experiments, and paywalls across both stores. Under the hood it uses StoreKit 2 on iOS and Billing Client 7 on Android.

In this tutorial, you will learn how to add in-app purchases with RevenueCat: configure products and entitlements, fetch an offering, present a paywall, and unlock content based on entitlement status. Key technologies: RevenueCat, StoreKit 2, Google Play Billing, entitlements.

Prerequisites

  • RevenueCat account (free tier available)
  • App Store Connect and/or Google Play Console access to create products
  • Flutter 3.x installed
  • A store product configured (e.g., a monthly subscription)

Core Content

Understand the RevenueCat model

RevenueCat’s mental model has three layers:

  1. Products — the raw SKUs in App Store Connect / Play Console (com.myapp.pro.monthly).
  2. Offerings — the products your app actually shows to users, grouped by paywall (default offering with monthly + yearly + lifetime). This lets you A/B test pricing without an app release.
  3. Entitlements — what a purchase means in your app (“pro”, “premium”). Multiple products can grant the same entitlement (monthly and yearly both unlock “pro”), and RevenueCat keeps it current or active.

Your code checks entitlements, never products — so re-pricing or swapping products doesn’t require a code change.

Configure products in both stores

Create the same products in both store consoles and link them in the RevenueCat dashboard. RevenueCat’s dashboard then maps them into your offering. Product identifiers must match the store identifiers exactly; the dashboard is where you decide which products belong to which offering.

Initialize the SDK

Add the dependency and configure RevenueCat before the app runs:

dependencies:
  purchases_flutter: ^8.0.0
import 'package:purchases_flutter/purchases_flutter.dart';

Future<void> init() async {
  await Purchases.setLogLevel(LogLevel.debug);
  await Purchases.configure(
    PurchasesConfiguration('your_revenuecat_public_api_key'),
  );
  // Restore the user's entitlement state (important for sign-in/out)
  final customerInfo = await Purchases.getCustomerInfo();
}

The public API key is safe to embed in a client — it can’t make purchases on its own.

Fetch an offering and present a paywall

Offerings drive the paywall. Fetch the default offering, then render its packages:

Future<Offering?> loadOffering() async {
  final offerings = await Purchases.getOfferings();
  return offerings.current;
}

// In the paywall widget:
final packages = offering?.availablePackages ?? [];

ListView.builder(
  itemCount: packages.length,
  itemBuilder: (context, index) {
    final pkg = packages[index];
    return ListTile(
      title: Text('${pkg.storeProduct.title}'),
      subtitle: Text(pkg.storeProduct.priceString),
      onTap: () => purchase(pkg),
    );
  },
);

Because offerings are fetched from the server, you can change prices, add a lifetime tier, or run an experiment — all without shipping a new build.

Purchase and check entitlements

Purchasing a package triggers the native store flow (StoreKit 2 sheet on iOS, Play Billing on Android):

Future<void> purchase(Package pkg) async {
  final info = await Purchases.purchasePackage(pkg);
  if (info.entitlements.active.isNotEmpty) {
    unlockPro();
  }
}

To restore purchases (required by both stores) or check status at any time:

// Restore (show a "Restore Purchases" button)
await Purchases.restorePurchases();

// Check entitlement
final info = await Purchases.getCustomerInfo();
final isPro = info.entitlements.active['pro'] != null;

When the user is logged in, map their RevenueCat app user ID to your backend user so subscriptions follow them across devices: await Purchases.logIn(userId).

Handle subscription lifecycle with webhooks

RevenueCat can push webhooks to your backend when subscriptions change — renewed, expired, refunded, billing issue, etc. A server receives events like SUBSCRIPTION_RENEWAL, SUBSCRIPTION_CANCELLATION, and NON_RENEWING_PURCHASE, and can revoke or grant server-side access in response. This is how you gate server-only features: your backend trusts the signed RevenueCat webhook, not the client.

Design the paywall

RevenueCat’s Paywalls are the off-the-shelf paywall builder: design once in the dashboard with templates (or your own HTML), then present it with a few lines. It ships with the same template on both platforms, letting you iterate on conversion without app updates.

Putting It All Together

A production setup: products configured in both stores and linked to an offering; RevenueCat initialized at startup; a paywall that fetches offerings.current and renders packages; entitlement checks (entitlements.active['pro']) gating features; a “Restore Purchases” button; and a webhook handler that keeps server-side access in sync. One SDK covers iOS and Android — no separate StoreKit and Play Billing code paths.

Conclusion & Next Steps

You’ve added in-app purchases with RevenueCat: offerings, paywalls, purchase and restore flows, entitlement gating, and webhook sync. Both storefronts are now one API.

Next Steps: run an experiment on pricing or paywall template in the RevenueCat dashboard, add an entitlement test for QA, and study the REST API to reconcile customer data with your own billing records.

References: