Flutter in-app purchases and subscriptions with purchases_flutter

To add in-app purchases to a Flutter app, install purchases_flutter, call await Purchases.setProxyURL('https://api.revenuedot.app') before Purchases.configure, then load offerings, call Purchases.purchase and check customerInfo.entitlements.active. RevenueDot is the backend: it verifies each App Store and Google Play purchase and answers with the customer's entitlements. The same Dart code runs on iOS and Android.

Architecture: a Flutter app using purchases_flutter talks to RevenueDot, which talks to the App Store and Google Play

What you need#

  • Flutter 3.22 or later. RevenueCat's Flutter installation guide lists it as the minimum, and says an iOS deployment target of 13.0 or higher must be set in ios/Podfile when you use CocoaPods.
  • An App Store Connect app with a subscription, and a Google Play Console app with a subscription. You can start with one platform.
  • A RevenueDot Cloud project. Sign up free.

The steps: create the products, connect the stores, build the catalog, install the package, configure it, build the paywall, test.

Step 1: Create the subscription in each store#

Create the same plan on each platform you ship.

  • App Store: in App Store Connect, create a subscription group and an auto-renewable subscription with a Product ID such as pro_monthly. Apple's help page lists each field.
  • Google Play: in Play Console, create a subscription with a Product ID, add a base plan with a billing period, set prices, and click Activate. Google's help page explains that hierarchy: subscription, then base plan, then pricing.

Write down both IDs. For Google Play subscriptions, RevenueDot's product ID is subscriptionId:basePlanId, for example pro:monthly.

For step-by-step store screens, see How to add subscriptions to a SwiftUI app and Android with Google Play Billing.

Step 2: Connect the stores to RevenueDot#

In RevenueDot, add one app per store.

Store What RevenueDot needs Guide
App Store Bundle ID, an In-App Purchase key (.p8, Key ID, Issuer ID), and the notification URL set as Version 2 in App Store Connect App Store
Google Play Package name, a service account JSON key with Play Console access, and a Pub/Sub push subscription for real-time developer notifications Google Play

The dashboard checks each credential and shows whether notifications arrive.

Step 3: Create the product, entitlement and offering#

  1. Products: add pro_monthly to the App Store app and pro:monthly to the Google Play app.
  2. Entitlements: add pro. Attach both products to it.
  3. Offerings: create default, add a $rc_monthly package, put both products in it (one per app), and make the offering current.

A package holds one product per app, so one offering serves both platforms. See offerings and packages.

RevenueDot dashboard page listing an offering with its packages and attached products

Step 4: Install the package#

Shell
flutter pub add purchases_flutter

Then confirm the iOS target in ios/Podfile:

Ruby
platform :ios, '13.0'

On Android, RevenueCat says to set the Activity's launchMode to standard or singleTop so that a purchase is not cancelled when the customer must authenticate in another app. Check android/app/src/main/AndroidManifest.xml.

The RevenueDot fork keeps the package name purchases_flutter. It is not published yet, and its patch branch needs native packages that are not published, so use the stock package in proxy mode today.

Step 5: Configure the SDK with the proxy URL#

Await setProxyURL before configure. The stock package defaults entitlementVerificationMode to disabled, which is what RevenueDot needs, so you change nothing else.

Dart
import 'dart:io' show Platform;
import 'package:purchases_flutter/purchases_flutter.dart';

Future<void> initPurchases() async {
  // Point the SDK at RevenueDot. Await it before configure.
  await Purchases.setProxyURL('https://api.revenuedot.app');
  await Purchases.configure(
    PurchasesConfiguration(Platform.isIOS ? 'appl_YourKey' : 'goog_YourKey'),
  );
}

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await initPurchases();
  runApp(const MyApp());
}

Five calls in order: setProxyURL, configure, getOfferings, purchase, then check entitlements

Never set EntitlementVerificationMode.enforced with the stock package. It checks signatures against RevenueCat's key, so every request would fail. See Trusted Entitlements.

Flutter web: the stock package's web plugin ignores setProxyURL, so web calls still go to RevenueCat. The RevenueDot fork fixes this, but it is not published. Target iOS and Android for now.

Step 6: Build the paywall#

Load the current offering, show its packages, buy, restore and read the entitlement.

Dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show PlatformException;
import 'package:purchases_flutter/purchases_flutter.dart';

class PaywallScreen extends StatefulWidget {
  const PaywallScreen({super.key});
  @override
  State<PaywallScreen> createState() => _PaywallScreenState();
}

class _PaywallScreenState extends State<PaywallScreen> {
  List<Package> packages = [];
  bool isPro = false;
  String? error;

  @override
  void initState() {
    super.initState();
    Purchases.addCustomerInfoUpdateListener(_apply);
    _load();
  }

  void _apply(CustomerInfo info) {
    if (!mounted) return;
    setState(() => isPro = info.entitlements.active.containsKey('pro'));
  }

  Future<void> _load() async {
    try {
      final offerings = await Purchases.getOfferings();
      _apply(await Purchases.getCustomerInfo());
      setState(() => packages = offerings.current?.availablePackages ?? []);
    } on PlatformException catch (e) {
      setState(() => error = e.message);
    }
  }

  Future<void> _buy(Package package) async {
    try {
      final result = await Purchases.purchase(PurchaseParams.package(package));
      _apply(result.customerInfo);
    } on PlatformException catch (e) {
      final code = PurchasesErrorHelper.getErrorCode(e);
      if (code != PurchasesErrorCode.purchaseCancelledError) {
        setState(() => error = e.message);
      }
    }
  }

  Future<void> _restore() async => _apply(await Purchases.restorePurchases());

  @override
  Widget build(BuildContext context) {
    if (isPro) return const Center(child: Text('You have Pro'));
    return ListView(padding: const EdgeInsets.all(16), children: [
      for (final p in packages)
        FilledButton(
          onPressed: () => _buy(p),
          child: Text('${p.storeProduct.title}, ${p.storeProduct.priceString}'),
        ),
      TextButton(onPressed: _restore, child: const Text('Restore purchases')),
      if (error != null) Text(error!, style: const TextStyle(color: Colors.red)),
    ]);
  }
}

Purchases.purchasePackage still works but is deprecated in the 10.x line. The PurchaseParams.package form is the current one.

Expected output: the screen lists one button per package. After a sandbox purchase, isPro turns true, and the dashboard shows the customer with an active pro entitlement and an INITIAL_PURCHASE event.

Optional: identify your own users#

Without an app user ID, the SDK starts with an anonymous ID (it begins with $RCAnonymousID:) and keeps it on the device. That is fine for an app with no accounts. If your app has sign-in, call logIn after the customer signs in, so their purchases follow them to a new phone and to your web app.

Dart
// After your own sign-in succeeds:
final result = await Purchases.logIn(myUserId);
final isPro = result.customerInfo.entitlements.active.containsKey('pro');

// When the customer signs out:
await Purchases.logOut();

When an anonymous customer who already bought something calls logIn, their purchases move to the identified user. RevenueDot keeps one customer record per app user ID and lists the old anonymous ID as an alias. The rules for who owns a purchase that two accounts both restore are the project's transfer behavior. They are explained in customers and app user IDs and the restore guide.

Use the same user ID on every platform, and never put a secret in it. The ID appears in dashboards, webhooks and logs.

Optional: move an existing RevenueCat app to RevenueDot#

If your Flutter app already ships purchases_flutter with RevenueCat's backend, the change is three lines. Add the proxy URL before configure, keep your current keys if you ran the importer, and call syncPurchases once on the first launch of the update, so subscribers who bought while the app talked to RevenueCat keep access.

Dart
await Purchases.setProxyURL('https://api.revenuedot.app');
await Purchases.configure(PurchasesConfiguration(Platform.isIOS ? 'appl_YourKey' : 'goog_YourKey'));
// Once, after this update:
await Purchases.syncPurchases();

Old app versions keep calling RevenueCat until their owners update, so run both systems side by side for a while. The migration post gives the order of steps.

Step 7: Test#

With no store account. Create a Test Store app in RevenueDot, add a product and a Test Store price, and pass its test_ key to configure. The SDK shows a Test Store dialog. Tap Test valid purchase. Test Store keys work only in debug builds, so ship with the appl_ and goog_ keys. For a local server, the iOS simulator reaches your Mac at localhost and the Android emulator at 10.0.2.2.

On iOS. Create a sandbox tester under Users and Access, Sandbox in App Store Connect and sign in with it under Settings, App Store, Sandbox Account. Sandbox subscriptions renew in minutes.

On Android. Add license testers in Play Console under Setup, License testing, publish the app to an internal test track, and install from that track. Test subscriptions renew every few minutes.

Before you ship, run one sandbox purchase on each store and check that it reaches the customer page and your webhooks. See sandbox testing.

Common errors and fixes#

Symptom Cause Fix
Offerings are empty Product IDs differ between the store and RevenueDot, or the offering is not current Match the IDs, make the offering current
Web calls reach RevenueCat The stock web plugin ignores setProxyURL Use iOS and Android, or the fork when it is published
Every request fails with a signature error entitlementVerificationMode set to enforced Remove it. Disabled is the default
Purchase succeeds on Android, entitlement missing No service account on the Google Play app Add it. RevenueDot answers 503 (code 7101) until then and the SDK retries
iOS build fails after you add the fork The fork needs unpublished native packages Use the stock package in proxy mode

Do it with RevenueDot#

  1. Create a free account. Cloud is free up to $10,000 in monthly tracked revenue.
  2. Add your App Store and Google Play apps with their credentials.
  3. Create the product, the pro entitlement and the default offering.
  4. Add await Purchases.setProxyURL('https://api.revenuedot.app') before configure.
  5. Add a webhook under Integrations, Webhooks so your own backend hears about purchases.

Start free on RevenueDot Cloud

FAQ#

Which Flutter package do I use for subscriptions?#

Use purchases_flutter, the RevenueCat SDK package. It works with RevenueCat's own backend and with RevenueDot when you call Purchases.setProxyURL before configure.

Does purchases_flutter work with a backend other than RevenueCat?#

Yes, through the proxy URL. RevenueDot speaks the same API, so the SDK talks to https://api.revenuedot.app or to your own server and your Dart code does not change.

Do I need the in_app_purchase package as well?#

No. purchases_flutter wraps StoreKit and Google Play Billing for you, and it handles verification through the backend. Use one purchase package, not two.

Can I use one entitlement for iOS and Android?#

Yes. Attach the App Store product and the Google Play product to the same entitlement, and put both in one package. Your app checks entitlements.active['pro'] on both platforms.

Does it work on Flutter web?#

Not in proxy mode with the stock package, because its web plugin ignores the proxy URL. RevenueDot's fork fixes it, but the fork is not published yet.

About RevenueDot. RevenueDot is an open-source (AGPL-3.0) backend for in-app purchases and subscriptions that works with the RevenueCat SDK. Start free on RevenueDot Cloud, free up to $10,000 in monthly tracked revenue, or self-host it with Docker and Postgres. Point the SDK's proxy URL at RevenueDot and keep your app code, your offerings and your customers. Read the quickstart or the code on GitHub.