# How to Create Custom Payments in FlutterFlow

- Tool: FlutterFlow
- Difficulty: Beginner
- Time required: 35-50 min
- Compatibility: FlutterFlow Free+ (Stripe Checkout); FlutterFlow Pro (custom CardField requires code export for App Store)
- Last updated: March 2026

## TL;DR

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.

## Before you start

- 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

### 1. 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.

### 2. 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.

```
// functions/index.js — Stripe Checkout session creator
// Install: cd functions && npm install stripe
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const Stripe = require('stripe');

admin.initializeApp();

exports.createCheckoutSession = functions.https.onRequest(async (req, res) => {
  res.set('Access-Control-Allow-Origin', '*');
  if (req.method === 'OPTIONS') { res.status(204).send(''); return; }

  const stripe = new Stripe(functions.config().stripe.secret_key);
  const {
    priceId,
    quantity = 1,
    successUrl,
    cancelUrl,
    customerId,
    userId,          // Firebase Auth UID for post-payment webhook
    mode = 'payment' // 'payment' or 'subscription'
  } = req.body;

  try {
    const session = await stripe.checkout.sessions.create({
      mode,
      line_items: [{ price: priceId, quantity }],
      success_url: successUrl ||
        'https://your-app.com/payment-success?session_id={CHECKOUT_SESSION_ID}',
      cancel_url: cancelUrl ||
        'https://your-app.com/payment-cancelled',
      customer: customerId || undefined,
      metadata: { userId }, // Pass userId for webhook
      ...(mode === 'subscription' && {
        subscription_data: { metadata: { userId } },
      }),
    });

    res.json({
      sessionId: session.id,
      sessionUrl: session.url,
    });
  } catch (err) {
    console.error('Checkout session error:', err);
    res.status(500).json({ error: err.message });
  }
});

// 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.

### 3. 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).

```
// Custom Action: initializePurchases
// Add to Pubspec: in_app_purchase: ^3.2.0
import 'dart:async';
import 'package:in_app_purchase/in_app_purchase.dart';

// Call once on app init or on the pricing page
Future<void> initializePurchases(
  Future Function(PurchaseDetails) onPurchaseSuccess,
) async {
  final bool available = await InAppPurchase.instance.isAvailable();
  if (!available) {
    // Store not available (e.g., no Google Play on emulator)
    return;
  }

  // Listen to purchase updates
  InAppPurchase.instance.purchaseStream.listen(
    (List<PurchaseDetails> purchaseDetailsList) async {
      for (final purchase in purchaseDetailsList) {
        if (purchase.status == PurchaseStatus.purchased ||
            purchase.status == PurchaseStatus.restored) {
          // Verify receipt server-side via Cloud Function
          // then deliver the entitlement
          await onPurchaseSuccess(purchase);
          // MUST call completePurchase to finish the transaction
          await InAppPurchase.instance.completePurchase(purchase);
        } else if (purchase.status == PurchaseStatus.error) {
          // Handle error
          print('Purchase error: ${purchase.error}');
        }
      }
    },
  );
}

// Custom Action: purchaseProduct
Future<void> purchaseProduct(String productId) async {
  final ProductDetailsResponse response =
      await InAppPurchase.instance.queryProductDetails({productId});

  if (response.notFoundIDs.isNotEmpty) {
    throw Exception('Product not found: $productId');
  }

  final ProductDetails product = response.productDetails.first;
  final PurchaseParam params = PurchaseParam(productDetails: product);

  // Use buyConsumable for one-time consumables (coins, credits)
  // Use buyNonConsumable for permanent unlocks
  await InAppPurchase.instance.buyNonConsumable(purchaseParam: params);
}
```

**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.

### 4. 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.

### 5. 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 code example

File: `Payment Implementation Decision Guide`

```text
Payment Method Decision Framework
====================================

Digital goods / subscriptions on iOS or Android?
  → MUST USE: in_app_purchase (Apple/Google mandate)
  → Apple takes 15-30%, Google takes 15-30%
  → Set up in: App Store Connect + Google Play Console

Physical goods, services, SaaS, web app?
  → USE: Stripe Checkout (simplest) OR
  → Stripe Payment Intent + CardField (custom UI)

Priority: simplicity and PCI compliance delegation?
  → Stripe Checkout (recommended for most apps)

Priority: in-app UX without browser redirect?
  → Stripe Payment Intent + flutter_stripe CardField

Audience strongly prefers PayPal?
  → PayPal via WebView (JS SDK checkout flow)

Stripe Checkout Architecture
==============================
FlutterFlow → createCheckoutSession CF
  Input:  priceId, quantity, userId, mode
  Output: sessionUrl

FlutterFlow → Launch URL (sessionUrl)
  → Stripe-hosted payment page
  → After payment → redirect to successUrl

Stripe → stripeWebhook CF
  Events: checkout.session.completed
  Action: update Firestore users/{uid}
    subscriptionStatus: 'active'
    subscriptionTier: 'pro'

FlutterFlow → real-time Firestore listener
  → UI updates automatically when Firestore changes

In-App Purchase Architecture
==============================
App Store Connect / Google Play Console
  → Create product (com.app.premium_monthly)
  → Set price and trial period

FlutterFlow Custom Actions:
  initializePurchases() → listen to purchase stream
  purchaseProduct(productId) → trigger native sheet
  restorePurchases() → required for App Store

Cloud Function: verifyReceipt
  → POST to Apple verifyReceipt / Google Play API
  → Update Firestore on valid receipt

Stripe Test Cards
==================
Success:  4242 4242 4242 4242
Declined: 4000 0000 0000 0002
3DS required: 4000 0025 0000 3155
Expiry: any future date, CVC: any 3 digits
```

## Common mistakes

- **Building a custom HTML input form for card number entry in the Flutter app instead of using Stripe's CardField widget** — If your app captures raw card numbers, even temporarily, you are handling cardholder data and must comply with PCI DSS SAQ D — the most extensive compliance level, requiring annual security assessments, network scans, and penetration testing. App stores reject apps that handle raw card numbers without proper PCI certification. Stripe's CardField tokenizes the card number on-device before it ever reaches your code. Fix: 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.
- **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** — Apple App Store Review Guideline 3.1.1 and Google Play's payment policy require all digital goods and subscriptions to use the platform's in-app purchase system. An app using Stripe for digital subscriptions will be rejected during App Store review. Even if it passes review initially, Apple or Google can remove the app and suspend your developer account for policy violations. Fix: 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.
- **Granting premium access client-side immediately after the in-app purchase callback without server-side receipt verification** — In-app purchase callbacks can be spoofed on jailbroken/rooted devices using tools that simulate purchase success events. Granting access before server-side verification means paying users can be spoofed by free users who fake the purchase callback. Fix: 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'

## 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.

---

Source: https://www.rapidevelopers.com/flutterflow-tutorials/how-can-i-create-custom-payments-in-flutterflow
© RapidDev — https://www.rapidevelopers.com/flutterflow-tutorials/how-can-i-create-custom-payments-in-flutterflow
