If your Flutter app sells digital features, credits or a subscription inside an iPhone or Android app, the stores require their own payment systems. In Flutter that means the in_app_purchase package: load your products, start a purchase, wait for the result on purchaseStream, verify it on a server, then call completePurchase.
This guide is for founders and developers about to monetize a mobile app. It uses in_app_purchase 3.3.1 (published by flutter.dev, October 2026) and a running example: a language app that sells a monthly subscription and a 50-lesson pack.
In short:
- Digital goods sold inside iOS and Android apps must use the store's billing.
buyNonConsumablesells subscriptions and permanent features.buyConsumablesells credits.- Verify every purchase on your server. The phone alone proves nothing.
- The package supports Android, iOS and macOS, not web.
The stores require their own billing
Apple's guideline 3.1.1 says features such as subscriptions, in-game currencies or premium content must be sold through in-app purchase. Credits bought this way may not expire, and you need a restore mechanism.
Google's Payments policy says apps taking payment for in-app features, digital content or goods must use Google Play's billing, with listed exceptions. Physical goods and real-world services are different.
Both policies also limit steering users to other payment methods, with regional exceptions. Read the current text of the App Store Review Guidelines before you ship, because it changes.
Create the products in both stores first
Nothing can be loaded until each store knows about your products. Create them in App Store Connect and in the Play Console, with the same product ids if you can, such as lessons_50 and pro_monthly.
Subscriptions have extra rules. Apple's guideline 3.1.2 requires an auto-renewable subscription to give ongoing value, last at least seven days, and work across the user's devices.
Load, buy, listen
The flow below follows the package's own readme:
final iap = InAppPurchase.instance;
final sub = iap.purchaseStream.listen(_onPurchases); // start in initState
final res = await iap.queryProductDetails({'lessons_50', 'pro_monthly'});
final matches = res.productDetails.where((p) => p.id == 'pro_monthly');
if (matches.isEmpty) return; // Show an unavailable-product message in the UI.
final product = matches.single;
final param = PurchaseParam(productDetails: product);
iap.buyNonConsumable(purchaseParam: param); // subscription or unlock
// iap.buyConsumable(purchaseParam: param); // credits
buyConsumable and buyNonConsumable do not return the outcome. The result arrives on purchaseStream, so subscribe when the app starts, not when the user taps. Check res.notFoundIDs too: an id missing in the store shows up there.
Handle each result, then complete it
In your listener, handle five states: pending, error, canceled, purchased and restored; clear the pending UI when a purchase is canceled. For the last two, verify, deliver the product, and if pendingCompletePurchase is true, call completePurchase. The package warns that on Android, a purchase not completed within 3 days is refunded. On iOS it is re-delivered on every launch, and buying the same product again fails.
Verify on a server, not on the phone
A purchase on the device is a claim. The package says to validate purchases following each store's best practices and links to the App Store and Google Play docs. Your server calls the store, checks the receipt or token, then grants credits or marks the account as subscribed.
One detail matters: on iOS and macOS the package now uses StoreKit 2 by default, which hands you signed data, not the old base64 receipt. Do not send StoreKit 2 signed transaction data to Apple's legacy receipt-validation endpoint. Make your server accept the format you actually receive, and test with sandbox purchases.
Restore purchases or get rejected
Call restorePurchases() from a visible "Restore" button. The package notes that the App Store has no API for querying consumables, and Google Play stops returning them once consumed. To restore credits on a new phone, keep your own record on your server.
When RevenueCat is worth it
RevenueCat publishes purchases_flutter (10.17.0, supports Android, iOS, macOS and web). It adds a hosted backend that checks receipts, tracks subscription state and gives one entitlement model across stores. It is a paid service, so compare its current pricing.
Choose it if you do not want to run receipt checks or renewals yourself. Choose in_app_purchase if you want fewer vendors and already have a backend.
Generated apps use Stripe Payment Links
Apps built with GrowApps take payments through Stripe Checkout, opened in a browser tab from a Stripe Payment Link. The builder never puts a card form in the app.
A generated app cannot verify a payment on a server yet, so the builder treats this as fit for tips, deposits or off-app products, not for unlocking paid features. If you sell digital features in the App Store or Google Play, the store rules above apply. See how to publish to Google Play.
Load one test product from the store
Create one consumable product in the Play Console, add a license tester account, and call queryProductDetails with its id. If notFoundIDs is empty and the price prints, your store setup is right.