Skip to content

· 6 min read · Nadeera Silva

Payments

Flutter In-App Purchase Guide: StoreKit 2, Play Billing and RevenueCat

A Flutter in-app purchase guide for 2026: a subscription example, server verification on Firebase, testing, cancelling, and when RevenueCat is the better call.

Smartphone on an off-white desk showing a teal shopping bag with a renewal loop, wired to a small server rack beside a teal shield with a checkmark, illustrating in-app purchases verified on a server

There are two sensible ways to add a Flutter in-app purchase today: the official in_app_purchase package with your own server checks, or RevenueCat. I've used both. This guide shows each one end to end, with a subscription example, server verification on Firebase, store notifications, testing and cancellation, all updated for StoreKit 2 and Apple's deprecation of verifyReceipt.

Which Flutter in-app purchase package should you use?

in_app_purchase + your backendRevenueCat (purchases_flutter)
Who verifies purchasesYour server, with Apple's and Google's APIsRevenueCat's servers
Server code you maintainVerification, notifications, entitlementsOptional webhook handler
Store API changesYou update for each oneRevenueCat ships SDK updates
Where purchase data livesYour databaseRevenueCat, with webhooks and exports
CostHosting and your timeFree up to $2,500 monthly tracked revenue, then 1% (pricing page, October 2026)

My rule of thumb: if you already run a backend and sell one or two products, the official package is very manageable. If you want to ship this week, or plan to test paywalls and prices, start with RevenueCat.

What changed on iOS: StoreKit 2 and the end of verifyReceipt

Many Flutter in-app purchase tutorials still say to post the app receipt to Apple's verifyReceipt endpoint. Apple's App Store Receipts documentation now marks receipts and that endpoint as deprecated. The replacement is the App Store Server API, or verifying the signed transactions StoreKit 2 already gives you.

Restores changed too. restoreCompletedTransactions() is deprecated as of iOS 18 in favour of AppStore.sync(), and Apple says to call sync() only from an explicit user action, because it can show a sign-in prompt. In the Flutter package, restorePurchases() handles this for you.

Flutter in-app purchase subscription example

Setup before any code:

  1. Create the same product IDs in App Store Connect and Play Console. On iOS, put subscriptions in a subscription group.
  2. On iOS, the Paid Apps Agreement must be active, with banking and tax details, before you can even test in the sandbox.
  3. Add in_app_purchase to pubspec.yaml (3.3.1 at the time of writing).

Then query products, start the purchase and listen to the stream. Subscriptions use buyNonConsumable:

import 'package:in_app_purchase/in_app_purchase.dart';

final iap = InAppPurchase.instance;
const productIds = {'pro_monthly', 'pro_yearly'};

Future<ProductDetails?> loadMonthly() async {
  if (!await iap.isAvailable()) return null;
  final res = await iap.queryProductDetails(productIds);
  if (res.notFoundIDs.isNotEmpty) {
    // IDs that don't match the store config end up here, not as an error
    debugPrint('Missing products: ${res.notFoundIDs}');
  }
  return res.productDetails.where((p) => p.id == 'pro_monthly').firstOrNull;
}

Future<void> buy(ProductDetails product, String userUuid) {
  return iap.buyNonConsumable(
    purchaseParam: PurchaseParam(
      productDetails: product,
      applicationUserName: userUuid, // becomes appAccountToken on StoreKit 2
    ),
  );
}

Listen to purchaseStream from app start, so purchases that finish while a screen is closed aren't lost. Send each purchase to your server and complete it only after the server accepts it:

import 'package:cloud_functions/cloud_functions.dart';

final verify = FirebaseFunctions.instance.httpsCallable('verifyPurchase');

void listenToPurchases() {
  iap.purchaseStream.listen((purchases) async {
    for (final p in purchases) {
      if (p.status == PurchaseStatus.purchased ||
          p.status == PurchaseStatus.restored) {
        await verify.call({
          'source': p.verificationData.source, // 'app_store' or 'google_play'
          'payload': p.verificationData.serverVerificationData,
        });
      }
      if (p.pendingCompletePurchase) await iap.completePurchase(p);
    }
  });
}

How to verify in-app purchases on the server

Diagram: the Flutter app sends the signed transaction or purchase token to the backend, the backend verifies it with Apple or Google, writes an entitlement record with an expiry date, and the app asks the backend for access
The app reports a purchase. The server checks it with the store and records when access ends.

On iOS, verify the JWS with Apple's App Store Server Library (@apple/app-store-server-library, 3.1.0 at the time of writing). On Android, look the purchase token up with the Google Play Developer API. A Cloud Function that does both:

import { onCall, HttpsError } from "firebase-functions/v2/https";
import { getFirestore } from "firebase-admin/firestore";
import { Environment, SignedDataVerifier } from "@apple/app-store-server-library";
import { google } from "googleapis";

const apple = new SignedDataVerifier(
  loadAppleRootCAs(),     // Apple root certificates from apple.com/certificateauthority
  true,                   // online revocation checks
  Environment.PRODUCTION, // SANDBOX for TestFlight and test builds
  "com.example.app",
  1234567890              // your app's Apple ID, required in production
);
const play = google.androidpublisher({
  version: "v3",
  auth: new google.auth.GoogleAuth({ scopes: ["https://www.googleapis.com/auth/androidpublisher"] }),
});

export const verifyPurchase = onCall(async (req) => {
  const uid = req.auth?.uid;
  if (!uid) throw new HttpsError("unauthenticated", "Sign in first");
  const { source, payload } = req.data;
  let expiresAt: Date;

  if (source === "app_store") {
    const tx = await apple.verifyAndDecodeTransaction(payload);
    if (tx.revocationDate) throw new HttpsError("permission-denied", "Revoked");
    expiresAt = new Date(tx.expiresDate!);
  } else {
    const { data } = await play.purchases.subscriptionsv2.get({
      packageName: "com.example.app",
      token: payload,
    });
    expiresAt = new Date(data.lineItems![0].expiryTime!);
  }

  await getFirestore().doc(`entitlements/${uid}`)
    .set({ source, expiresAt, updatedAt: new Date() }, { merge: true });
  return { active: expiresAt > new Date() };
});

This handles subscriptions. One-time products use purchases.products on Google's side, and Apple transactions without an expiresDate. Whatever you sell, store when access ends and compare that date on the server.

Keep subscriptions current with store notifications

Renewals, refunds, billing problems and cancellations happen while the app is closed:

  • Apple sends App Store Server Notifications V2. Verify them with the same library's verifyAndDecodeNotification.
  • Google sends Real-time developer notifications through Cloud Pub/Sub.

Treat a notification as "something changed", then read the current state (getAllSubscriptionStatuses on Apple, subscriptionsv2.get on Google) and update the same entitlement document. My StoreKit subscriptions and webhooks post goes deeper on idempotency and restore.

RevenueCat Flutter integration

With RevenueCat, the server parts above are handled for you. Using purchases_flutter 10:

import 'package:purchases_flutter/purchases_flutter.dart';

Future<void> setUp(String apiKey, String firebaseUid) =>
    Purchases.configure(PurchasesConfiguration(apiKey)..appUserID = firebaseUid);

Future<bool> buyMonthly() async {
  final monthly = (await Purchases.getOfferings()).current?.monthly;
  if (monthly == null) return false;
  final result = await Purchases.purchase(PurchaseParams.package(monthly));
  return result.customerInfo.entitlements.active.containsKey('pro');
}

Future<bool> restore() async =>
    (await Purchases.restorePurchases()).entitlements.active.containsKey('pro');

purchasePackage still works but is deprecated in favour of purchase(PurchaseParams). Using your Firebase UID as the app user ID keeps RevenueCat's customer and your user in step.

How to test in-app purchases on iOS and Android

iOSAndroid
Test accountsSandbox Apple Account in App Store ConnectLicense testers in Play Console
Where it worksBuilds run from Xcode, and TestFlight buildsApps installed from a Play test track
GotchaPaid Apps Agreement must be activeTesters who aren't license testers are charged for real

Test at least: a first purchase, a restore on a second device, an expired subscription losing access, a refund removing access, and a purchase that's never completed on Android (it should be refunded after three days).

Letting users cancel a subscription

Users cancel in the store, not in your app, but you should make that easy. On iOS, InAppPurchaseStoreKitPlatformAddition.showManageSubscriptions() opens the App Store's management sheet (StoreKit 2, iOS 15+). On Android, Google documents a deep link to the Play subscriptions centre: https://play.google.com/store/account/subscriptions. With RevenueCat, customerInfo.managementURL gives you the right link per platform.

If your app also has Android users on Play Billing Library 7, read the Play Billing deadline post. For keeping keys out of the app while you build this, see how to hide API keys in Flutter.

Frequently asked questions

Which Flutter in-app purchase package should I use?

The official in_app_purchase package if you run your own backend and want full control of purchase data, or RevenueCat's purchases_flutter if you want verification, notifications and entitlements handled for you.

How do I add a subscription with in_app_purchase?

Create the products in App Store Connect and Play Console, query them with queryProductDetails, buy with buyNonConsumable, listen to purchaseStream, verify on your server and then call completePurchase.

How do I verify in-app purchases on the server?

On iOS, verify the StoreKit 2 signed transaction with Apple's App Store Server Library or API. On Android, look up the purchase token with the Google Play Developer API, for subscriptions purchases.subscriptionsv2.get.

Can I still use verifyReceipt?

Apple's documentation marks app receipts and the verifyReceipt endpoint as deprecated. Verify StoreKit 2 signed transactions or call the App Store Server API instead.

How do I test in-app purchases on iOS and TestFlight?

Use a Sandbox Apple Account. Sandbox purchases work in builds run from Xcode and in TestFlight builds, and the Paid Apps Agreement must be active first.

How do I test in-app purchases on Android?

Add license testers in Play Console and install the app from a test track. Testers who aren't license testers are charged for real purchases.

How do users cancel a subscription in a Flutter app?

They cancel in the store. On iOS, InAppPurchaseStoreKitPlatformAddition.showManageSubscriptions() opens the App Store sheet. On Android, link to https://play.google.com/store/account/subscriptions.

What happens if I never acknowledge an Android purchase?

Google refunds and revokes purchases that are not acknowledged within three days. completePurchase in the Flutter plugin acknowledges it.

Sources

  1. App Store Receipts (deprecated) (Apple)
  2. App Store Server API (Apple)
  3. Simplifying your implementation by using the App Store Server Library (Apple)
  4. App Store Server Library for Node.js (GitHub)
  5. App Store Server Notifications (Apple)
  6. Transaction.currentEntitlements (Apple)
  7. AppStore.sync() (Apple)
  8. restoreCompletedTransactions() (deprecated) (Apple)
  9. Overview for configuring In-App Purchases (Apple)
  10. in_app_purchase_storekit changelog (pub.dev)
  11. Integrate the Google Play Billing Library into your app (Google)
  12. Fight fraud and abuse (Play Billing) (Google)
  13. Method: purchases.subscriptionsv2.get (Google)
  14. Real-time developer notifications reference guide (Google)
  15. purchases_flutter (pub.dev)
  16. RevenueCat pricing (RevenueCat)
  17. Testing In-App Purchases with sandbox (Apple)
  18. Test your Google Play Billing Library integration (Google)
  19. About subscriptions (link to the subscriptions center) (Google)
  20. in_app_purchase (pub.dev)
SharePost on XLinkedIn

Comments

Share a question or note about this article.

Loading comments…

Flutter In-App Purchase Guide: Subscriptions and RevenueCat | Nadeera Silva