· 6 min read · Nadeera Silva
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.

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 backend | RevenueCat (purchases_flutter) | |
|---|---|---|
| Who verifies purchases | Your server, with Apple's and Google's APIs | RevenueCat's servers |
| Server code you maintain | Verification, notifications, entitlements | Optional webhook handler |
| Store API changes | You update for each one | RevenueCat ships SDK updates |
| Where purchase data lives | Your database | RevenueCat, with webhooks and exports |
| Cost | Hosting and your time | Free 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:
- Create the same product IDs in App Store Connect and Play Console. On iOS, put subscriptions in a subscription group.
- On iOS, the Paid Apps Agreement must be active, with banking and tax details, before you can even test in the sandbox.
- Add
in_app_purchasetopubspec.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

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
| iOS | Android | |
|---|---|---|
| Test accounts | Sandbox Apple Account in App Store Connect | License testers in Play Console |
| Where it works | Builds run from Xcode, and TestFlight builds | Apps installed from a Play test track |
| Gotcha | Paid Apps Agreement must be active | Testers 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
- App Store Receipts (deprecated) (Apple)
- App Store Server API (Apple)
- Simplifying your implementation by using the App Store Server Library (Apple)
- App Store Server Library for Node.js (GitHub)
- App Store Server Notifications (Apple)
- Transaction.currentEntitlements (Apple)
- AppStore.sync() (Apple)
- restoreCompletedTransactions() (deprecated) (Apple)
- Overview for configuring In-App Purchases (Apple)
- in_app_purchase_storekit changelog (pub.dev)
- Integrate the Google Play Billing Library into your app (Google)
- Fight fraud and abuse (Play Billing) (Google)
- Method: purchases.subscriptionsv2.get (Google)
- Real-time developer notifications reference guide (Google)
- purchases_flutter (pub.dev)
- RevenueCat pricing (RevenueCat)
- Testing In-App Purchases with sandbox (Apple)
- Test your Google Play Billing Library integration (Google)
- About subscriptions (link to the subscriptions center) (Google)
- in_app_purchase (pub.dev)
Keep reading
- StoreKit 2 Subscriptions in Flutter: Restore Purchases and Server NotificationsStoreKit 2 subscriptions in a Flutter app: server verification, App Store Server Notifications V2, restore purchases and one access record across gateways.
- Play Billing Library Deadline: Updating Flutter IAP AppsGoogle Play's extension for Billing Library 7 and target API 36 ends on 1 Nov 2026. Here's how to upgrade Flutter in_app_purchase and retest it.
- Flutter Stripe Integration: PaymentSheet, Webhooks and ConnectIntegrate Stripe in Flutter with flutter_stripe: PaymentSheet, Apple Pay and Google Pay, webhooks that fulfil orders, and Stripe Connect for marketplaces.
Comments
Share a question or note about this article.
Loading comments…