Choose your payment method based on your use case: Stripe Checkout (Cloud Function creates CheckoutSession → Launch URL) for simplest PCI-compliant one-time payments, Stripe Payment Intent with CardField Custom Widget for in-app card entry without redirects, in_app_purchase package for App Store and Play Store native subscriptions, or PayPal via WebView for PayPal-preferred audiences. Never build a custom card form — always use Stripe's pre-built components.
Add payments to your FlutterFlow app using the right method for your platform and audience
Payment implementation is one of the most consequential technical decisions in any app. The wrong choice means either app store rejection, PCI compliance liability, or a checkout experience so friction-filled that users abandon. FlutterFlow supports four payment patterns, each with different trade-offs on complexity, UX quality, platform compatibility, and compliance requirements. This tutorial walks through all four — Stripe Checkout hosted page, Stripe Payment Intent with custom CardField, Apple/Google native in-app purchases, and PayPal via WebView — with the setup steps and a clear decision guide for which to use.
Prerequisites
- A Stripe account (stripe.com — free to sign up, test mode available without business verification)
- For in-app purchases: an Apple Developer account ($99/year) and/or Google Play Console account ($25 one-time) with your app already registered
- Firebase project with Cloud Functions enabled (Blaze plan) for Stripe server-side operations
- A FlutterFlow project with the page or flow where users will make a payment
Step-by-step guide
Choose your payment method using the decision framework
Choose your payment method using the decision framework
Answer these four questions to pick the right payment pattern. Question 1 — Will this app be sold on the App Store or Google Play Store, and do you charge for subscriptions or digital content? If yes: you MUST use in-app purchases (in_app_purchase package) for digital goods and subscriptions — Apple and Google mandate this and take 15-30%. Physical goods and services are exempt. Question 2 — Do you need full payment UI inside your app with no redirect? If yes: use Stripe Payment Intent + CardField Custom Widget (available in FlutterFlow via Custom Widget with Stripe's Flutter SDK). Note: App Store and Google Play do not allow custom card entry forms for digital goods — only in-app purchase. Question 3 — Is simplest possible integration the priority, or do you sell physical goods/services? If yes: use Stripe Checkout — a Cloud Function creates a CheckoutSession, you get a URL, Launch URL opens Stripe's hosted payment page. Stripe handles PCI compliance, address collection, fraud detection. Question 4 — Does your audience prefer PayPal over card entry? PayPal via WebView is viable for web-first audiences or international markets where PayPal is dominant.
Expected result: You have selected the appropriate payment pattern for your app's use case and platform distribution channel.
Implement Stripe Checkout — the simplest path to production payments
Implement Stripe Checkout — the simplest path to production payments
Stripe Checkout is a Stripe-hosted payment page — you create a session server-side and redirect the user to Stripe's URL. All card entry, fraud detection, address collection, and PCI compliance are handled by Stripe. Create a Cloud Function named createCheckoutSession. It receives: priceId (from your Stripe Dashboard product), quantity, successUrl, cancelUrl, and optionally customerId. The function calls Stripe's API to create a session and returns the URL. In FlutterFlow: add the createCheckoutSession API Call to your API Manager → TransactionService group. On your pricing or cart page, add a 'Buy Now' or 'Subscribe' button. Action Flow: call createCheckoutSession with the price ID and quantity, get the session URL from the response, then add a Launch URL action using the returned sessionUrl. The user is taken to Stripe's hosted page, pays, and Stripe redirects back to your successUrl. Create a Success page in FlutterFlow and use deep links or Firebase Dynamic Links to bring the user back into the app after payment.
1// functions/index.js — Stripe Checkout session creator2// Install: cd functions && npm install stripe3const functions = require('firebase-functions');4const admin = require('firebase-admin');5const Stripe = require('stripe');67admin.initializeApp();89exports.createCheckoutSession = functions.https.onRequest(async (req, res) => {10 res.set('Access-Control-Allow-Origin', '*');11 if (req.method === 'OPTIONS') { res.status(204).send(''); return; }1213 const stripe = new Stripe(functions.config().stripe.secret_key);14 const {15 priceId,16 quantity = 1,17 successUrl,18 cancelUrl,19 customerId,20 userId, // Firebase Auth UID for post-payment webhook21 mode = 'payment' // 'payment' or 'subscription'22 } = req.body;2324 try {25 const session = await stripe.checkout.sessions.create({26 mode,27 line_items: [{ price: priceId, quantity }],28 success_url: successUrl ||29 'https://your-app.com/payment-success?session_id={CHECKOUT_SESSION_ID}',30 cancel_url: cancelUrl ||31 'https://your-app.com/payment-cancelled',32 customer: customerId || undefined,33 metadata: { userId }, // Pass userId for webhook34 ...(mode === 'subscription' && {35 subscription_data: { metadata: { userId } },36 }),37 });3839 res.json({40 sessionId: session.id,41 sessionUrl: session.url,42 });43 } catch (err) {44 console.error('Checkout session error:', err);45 res.status(500).json({ error: err.message });46 }47});4849// Set config: firebase functions:config:set stripe.secret_key='sk_test_...'Expected result: Buy button calls createCheckoutSession, receives a URL, and launches Stripe's hosted payment page. Test with Stripe's test card 4242 4242 4242 4242 / any future date / any 3-digit CVC.
Implement in-app purchases for App Store and Google Play subscriptions
Implement in-app purchases for App Store and Google Play subscriptions
If your app has subscriptions or sells digital content (premium features, coins, additional storage) and will be distributed on the App Store or Google Play, you must use in-app purchases — Apple and Google require it for all digital goods sold in mobile apps. In FlutterFlow: Custom Code → Pubspec Dependencies → add in_app_purchase at the current version. Create a Custom Action named initializePurchases that sets up the purchase stream and listens for purchase updates. Create a Custom Action named purchaseProduct that takes a productId (the ID you created in App Store Connect or Google Play Console) and calls InAppPurchase.instance.buyNonConsumable or buyConsumable. Create a Custom Action named restorePurchases for the required 'Restore Purchases' button (App Store requirement). Configure products: in App Store Connect → your app → In-App Purchases → add a product with identifier (e.g., com.yourapp.premium_monthly). In Google Play Console → Monetize → Subscriptions. Test using Sandbox accounts (App Store) or test accounts (Google Play).
1// Custom Action: initializePurchases2// Add to Pubspec: in_app_purchase: ^3.2.03import 'dart:async';4import 'package:in_app_purchase/in_app_purchase.dart';56// Call once on app init or on the pricing page7Future<void> initializePurchases(8 Future Function(PurchaseDetails) onPurchaseSuccess,9) async {10 final bool available = await InAppPurchase.instance.isAvailable();11 if (!available) {12 // Store not available (e.g., no Google Play on emulator)13 return;14 }1516 // Listen to purchase updates17 InAppPurchase.instance.purchaseStream.listen(18 (List<PurchaseDetails> purchaseDetailsList) async {19 for (final purchase in purchaseDetailsList) {20 if (purchase.status == PurchaseStatus.purchased ||21 purchase.status == PurchaseStatus.restored) {22 // Verify receipt server-side via Cloud Function23 // then deliver the entitlement24 await onPurchaseSuccess(purchase);25 // MUST call completePurchase to finish the transaction26 await InAppPurchase.instance.completePurchase(purchase);27 } else if (purchase.status == PurchaseStatus.error) {28 // Handle error29 print('Purchase error: ${purchase.error}');30 }31 }32 },33 );34}3536// Custom Action: purchaseProduct37Future<void> purchaseProduct(String productId) async {38 final ProductDetailsResponse response =39 await InAppPurchase.instance.queryProductDetails({productId});4041 if (response.notFoundIDs.isNotEmpty) {42 throw Exception('Product not found: $productId');43 }4445 final ProductDetails product = response.productDetails.first;46 final PurchaseParam params = PurchaseParam(productDetails: product);4748 // Use buyConsumable for one-time consumables (coins, credits)49 // Use buyNonConsumable for permanent unlocks50 await InAppPurchase.instance.buyNonConsumable(purchaseParam: params);51}Expected result: Tapping 'Subscribe' on iOS/Android triggers the native App Store or Google Play purchase sheet. On successful purchase, the purchase stream fires the success callback.
Use Stripe PaymentIntent with CardField for in-app card entry
Use Stripe PaymentIntent with CardField for in-app card entry
If you need card entry inside your app (no redirect to a browser), use Stripe's Flutter SDK with the CardField widget — this is the only PCI-compliant way to accept card numbers in a custom UI. The CardField widget is provided by the flutter_stripe package and tokenizes the card data on-device before it ever leaves the phone — your Cloud Function never sees the raw card number. In FlutterFlow: Custom Code → Pubspec Dependencies → add flutter_stripe. Create a Custom Widget that embeds the CardField. The workflow: (1) Cloud Function creates a PaymentIntent and returns the clientSecret, (2) the CardField captures the card details, (3) the Custom Action calls Stripe.instance.confirmPayment(clientSecret, params) to process the payment, (4) on success the Custom Action returns the paymentIntentId to FlutterFlow. This pattern requires FlutterFlow Pro for downloading and testing the compiled custom widget, and additional Stripe SDK configuration in the iOS and Android platform files.
Expected result: A CardField widget appears inline in your checkout page. Users enter card details and tap Pay — the payment processes in-app without a browser redirect.
Handle payment confirmation with a Stripe webhook
Handle payment confirmation with a Stripe webhook
Regardless of which payment method you choose, confirm payment server-side via a webhook before granting access to paid features. Never trust the client to report its own payment status. After a Stripe Checkout payment completes, Stripe fires checkout.session.completed and payment_intent.succeeded webhooks to your Cloud Function endpoint (registered in Stripe Dashboard → Developers → Webhooks). Your Cloud Function handler: verify the Stripe-Signature header, extract the userId from session.metadata, update the user's Firestore document: subscriptionStatus: 'active', subscriptionTier: 'pro', subscriptionExpiry: (current date + 30 days for monthly). In FlutterFlow, the user's profile page has a real-time Backend Query on the users/{uid} document — when the webhook writes the update, FlutterFlow's listener sees it within 1-2 seconds and the UI updates to show the unlocked premium features automatically.
Expected result: After successful payment in Stripe's hosted checkout, the user's FlutterFlow app shows premium features unlocked within 2-3 seconds without any manual refresh.
Complete working example
1Payment Method Decision Framework2====================================34Digital goods / subscriptions on iOS or Android?5 → MUST USE: in_app_purchase (Apple/Google mandate)6 → Apple takes 15-30%, Google takes 15-30%7 → Set up in: App Store Connect + Google Play Console89Physical goods, services, SaaS, web app?10 → USE: Stripe Checkout (simplest) OR11 → Stripe Payment Intent + CardField (custom UI)1213Priority: simplicity and PCI compliance delegation?14 → Stripe Checkout (recommended for most apps)1516Priority: in-app UX without browser redirect?17 → Stripe Payment Intent + flutter_stripe CardField1819Audience strongly prefers PayPal?20 → PayPal via WebView (JS SDK checkout flow)2122Stripe Checkout Architecture23==============================24FlutterFlow → createCheckoutSession CF25 Input: priceId, quantity, userId, mode26 Output: sessionUrl2728FlutterFlow → Launch URL (sessionUrl)29 → Stripe-hosted payment page30 → After payment → redirect to successUrl3132Stripe → stripeWebhook CF33 Events: checkout.session.completed34 Action: update Firestore users/{uid}35 subscriptionStatus: 'active'36 subscriptionTier: 'pro'3738FlutterFlow → real-time Firestore listener39 → UI updates automatically when Firestore changes4041In-App Purchase Architecture42==============================43App Store Connect / Google Play Console44 → Create product (com.app.premium_monthly)45 → Set price and trial period4647FlutterFlow Custom Actions:48 initializePurchases() → listen to purchase stream49 purchaseProduct(productId) → trigger native sheet50 restorePurchases() → required for App Store5152Cloud Function: verifyReceipt53 → POST to Apple verifyReceipt / Google Play API54 → Update Firestore on valid receipt5556Stripe Test Cards57==================58Success: 4242 4242 4242 424259Declined: 4000 0000 0000 0002603DS required: 4000 0025 0000 315561Expiry: any future date, CVC: any 3 digitsCommon mistakes when creating Custom Payments in FlutterFlow
Why it's a problem: Building a custom HTML input form for card number entry in the Flutter app instead of using Stripe's CardField widget
How to avoid: Use Stripe's flutter_stripe package and embed the CardField widget in your Custom Widget. CardField never exposes the raw card number to your app code — it returns a PaymentMethod token. This qualifies you for PCI SAQ A-EP, the simplest compliance path. Alternatively, use Stripe Checkout which gives you PCI SAQ A, the simplest of all.
Why it's a problem: Selling digital subscriptions or in-app content using Stripe Checkout instead of Apple/Google in-app purchases in an app distributed on the App Store or Google Play
How to avoid: Use the in_app_purchase Flutter package for any digital goods or subscriptions in apps distributed on App Store or Google Play. Stripe is allowed for physical goods (merchandise, food delivery), services (booking appointments), and in-app features that do not constitute 'digital content' under Apple/Google's definitions.
Why it's a problem: Granting premium access client-side immediately after the in-app purchase callback without server-side receipt verification
How to avoid: Send the purchase receipt (verificationData from PurchaseDetails) to a Cloud Function that calls Apple's App Store receipt verification endpoint or Google Play Developer API to validate the purchase before writing subscriptionStatus: 'active' to Firestore. Only grant access after the server confirms the receipt is genuine.
Best practices
- Use Stripe's pre-built components (Checkout, CardField) rather than building custom payment UIs — they handle PCI compliance, accessibility, and localization automatically
- Always verify payment success server-side via Stripe webhooks before granting premium access — never trust client-reported payment status
- Store your Stripe secret key in Firebase Functions config only — never in FlutterFlow API Manager, environment variables, or source code
- Use Stripe's test mode and test card numbers extensively before switching to live keys — test the full flow including webhook delivery
- For in-app purchases, always implement and test Restore Purchases — Apple requires this button and users on new devices need it to recover purchased content
- Show clear pricing before the payment step — unexpected price reveals cause cart abandonment and chargebacks
- Handle payment failures gracefully with user-friendly error messages: 'Your card was declined — please check the card number and expiry date' is better than 'Error 402'
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I am building a FlutterFlow app with Firebase and want to add Stripe Checkout for one-time payments. Write a Firebase Cloud Function in Node.js that creates a Stripe Checkout session with a priceId, quantity, userId in metadata, successUrl, and cancelUrl. Also write the Stripe webhook handler Cloud Function that listens for checkout.session.completed, verifies the signature, and updates the user's Firestore document with subscriptionStatus: 'active' and the tier from the product metadata.
Add a 'Get Pro' button on the Pricing page that calls the createCheckoutSession Cloud Function API with the monthly subscription price ID. On success, take the sessionUrl from the API response and use a Launch URL action to open Stripe's hosted checkout. After the user returns from checkout, show the updated subscription status from the Firestore user document.
Frequently asked questions
Does Apple take a cut of Stripe payments in my iOS app?
Apple only takes a cut (15-30%) of digital goods and subscriptions processed through their In-App Purchase system. Physical goods, services, and B2B SaaS are generally exempt. If you use Stripe Checkout for selling a physical product or a service (like a consulting session or delivery), Apple does not take a cut and you pay only Stripe's 2.9% + 30 cents. However, if you sell premium app features, virtual currency, or subscriptions to digital content and distribute on the App Store, you must use IAP and Apple takes their percentage.
How do I handle recurring subscriptions with Stripe?
Use Stripe Checkout in 'subscription' mode: set mode: 'subscription' in the createCheckoutSession call and use a recurring price ID (created in Stripe Dashboard → Products → Add product → pricing: recurring). Stripe handles all subsequent charges automatically. For cancellations: create a Cloud Function that calls stripe.subscriptions.cancel(subscriptionId) or use Stripe's hosted Customer Portal (stripe.billingportal.sessions.create) where users manage their own subscriptions. Set up webhook handlers for customer.subscription.updated and customer.subscription.deleted to update Firestore accordingly.
How do I show a paywall that gates premium features?
In FlutterFlow, read the user's Firestore document (users/{uid}) which has the subscriptionStatus and subscriptionTier fields set by your webhook handler. On gated pages or features, add a Conditional Widget: if subscriptionStatus == 'active' show the premium content, else show the paywall/upgrade prompt. Since this is backed by Firestore data, not a client-side flag, it cannot be bypassed by modifying app variables. The Firestore security rules allow users to read only their own document.
What is the difference between a PaymentIntent and a Checkout Session?
A CheckoutSession creates Stripe's hosted payment page — you get a URL and redirect the user there. Stripe handles the UI, PCI compliance, and all payment methods. A PaymentIntent is a lower-level object representing a payment to be confirmed — you build your own UI (using CardField) and confirm the intent client-side. CheckoutSession is simpler and handles more automatically. PaymentIntent gives you full UI control. For most FlutterFlow apps, CheckoutSession is the right choice.
Can I accept payments in multiple currencies?
Yes — Stripe supports 135+ currencies. In your createCheckoutSession Cloud Function, set currency on the line item or use Stripe's automatic currency conversion. Alternatively, create separate prices in different currencies in Stripe Dashboard and pass the appropriate priceId based on the user's location (from their device locale via Global Properties). Stripe displays the price in the user's currency on the hosted checkout page automatically.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation