# How to Add Payments to Your FlutterFlow Project

- Tool: FlutterFlow
- Difficulty: Beginner
- Time required: 45-60 min
- Compatibility: FlutterFlow Standard+ (Custom Actions required)
- Last updated: March 2026

## TL;DR

Add Stripe payments to FlutterFlow by deploying a Firebase Cloud Function that creates a Stripe Checkout Session, calling it from a Custom Action, and launching the returned URL with FlutterFlow's Launch URL action. A second webhook Cloud Function listens for checkout.session.completed to create order records in Firestore. Never put your Stripe secret key in FlutterFlow Custom Action code — it compiles into your app and becomes publicly visible.

## Why Stripe Needs a Cloud Function Middleman

FlutterFlow apps run on users' devices, which means any code you write is potentially visible to anyone who decompiles your app. Stripe's secret key (sk_test_xxx or sk_live_xxx) must never appear in your FlutterFlow Custom Action code — it would be exposed. The correct architecture uses a Firebase Cloud Function as a secure server: your FlutterFlow app calls the Cloud Function, the Cloud Function calls Stripe with the secret key (stored as a Firebase environment variable), and returns the checkout URL. Your app then opens that URL. Stripe handles the payment form, card security, and PCI compliance.

## Before you start

- A Stripe account (free at stripe.com) with at least test mode enabled
- A Firebase project with Blaze plan (Cloud Functions require billing enabled)
- FlutterFlow project connected to the Firebase project
- FlutterFlow Standard plan or higher (Custom Actions required)

## Step-by-step guide

### 1. Create a Stripe Account and Get Your API Keys

Sign up at stripe.com if you don't have an account. Verify your email. In the Stripe Dashboard, click Developers in the left sidebar, then API Keys. You'll see two key types: Publishable key (pk_test_xxx) — safe for client-side code, and Secret key (sk_test_xxx) — server-side only, never in app code. You're in test mode by default, which is correct for now. Copy your Secret key and save it somewhere secure (a password manager). In your Stripe Dashboard, also navigate to Products > Create a product to create at least one product with a price — you'll need the price ID (price_xxx) when creating Checkout Sessions.

**Expected result:** You have your Stripe test Secret key saved securely and at least one product with a price ID (price_xxx) in your Stripe account.

### 2. Deploy a Cloud Function to Create Checkout Sessions

In Firebase Console, go to Functions and click Get Started. Initialize functions in your terminal with 'firebase init functions' (select JavaScript). Install the Stripe SDK: 'npm install stripe' inside the functions/ directory. Set your Stripe secret key as a Firebase environment config variable: 'firebase functions:config:set stripe.secret_key=sk_test_xxx'. Deploy the createCheckoutSession function below. This function receives a price ID and user ID, creates a Stripe Checkout Session, and returns the session URL. Register the function URL in Stripe Webhooks after deployment.

```
const functions = require('firebase-functions');
const Stripe = require('stripe');
const cors = require('cors')({ origin: true });

exports.createCheckoutSession = functions.https.onRequest((req, res) => {
  cors(req, res, async () => {
    if (req.method !== 'POST') {
      return res.status(405).send('Method Not Allowed');
    }
    try {
      const stripe = Stripe(functions.config().stripe.secret_key);
      const { priceId, userId, successUrl, cancelUrl } = req.body;

      if (!priceId || !userId) {
        return res.status(400).json({ error: 'Missing priceId or userId' });
      }

      const session = await stripe.checkout.sessions.create({
        payment_method_types: ['card'],
        mode: 'payment', // or 'subscription' for recurring
        line_items: [{ price: priceId, quantity: 1 }],
        metadata: { userId },
        success_url: successUrl || 'https://yourapp.com/success',
        cancel_url: cancelUrl || 'https://yourapp.com/cancel',
      });

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

**Expected result:** Cloud Function is deployed and accessible at a URL like https://us-central1-your-project.cloudfunctions.net/createCheckoutSession.

### 3. Call the Cloud Function from a FlutterFlow Custom Action

In FlutterFlow, go to Custom Code > Custom Actions and create a new action called 'initiateStripeCheckout'. This action takes the priceId as a parameter, calls your Cloud Function, and returns the checkout URL. Add the 'http' package to your project dependencies. The action posts to your Cloud Function URL with the priceId and currentUserUID, receives the session URL in the response, and returns it as a String. Keep your Cloud Function URL in a project constant (not hardcoded in the action) for easy updates between environments.

```
import 'dart:convert';
import 'package:http/http.dart' as http;

Future<String> initiateStripeCheckout(String priceId) async {
  const cloudFunctionUrl =
      'https://us-central1-YOUR-PROJECT.cloudfunctions.net/createCheckoutSession';

  try {
    final response = await http.post(
      Uri.parse(cloudFunctionUrl),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode({
        'priceId': priceId,
        'userId': currentUserUid, // FlutterFlow built-in
        'successUrl': 'https://yourapp.com/payment-success',
        'cancelUrl': 'https://yourapp.com/payment-cancel',
      }),
    );

    if (response.statusCode == 200) {
      final data = jsonDecode(response.body);
      return data['sessionUrl'] as String;
    } else {
      debugPrint('Cloud Function error: ${response.statusCode} ${response.body}');
      return '';
    }
  } catch (e) {
    debugPrint('initiateStripeCheckout error: $e');
    return '';
  }
}
```

**Expected result:** The Custom Action calls your Cloud Function and returns a valid Stripe Checkout Session URL string.

### 4. Wire Up the Buy Button Action Chain

On your product detail page or pricing page in FlutterFlow, select the Buy Now button. Open its On Tap action chain. Add these actions in sequence: (1) Show Loading overlay, (2) Call the initiateStripeCheckout Custom Action (pass your price ID), (3) Hide Loading overlay, (4) Add a Conditional action: if the returned URL is not empty, launch it using FlutterFlow's Launch URL action (set to open in a new tab/browser). If the URL is empty, show a SnackBar with an error message. The Launch URL action opens the Stripe-hosted checkout page in the browser where the user enters card details.

**Expected result:** Tapping Buy Now shows a loading indicator, then opens the Stripe Checkout page in the browser with your product details and price displayed.

### 5. Deploy a Webhook to Create Orders After Payment

After Stripe processes a payment, your app needs to know about it. Create a second Cloud Function that receives Stripe webhook events and writes order records to Firestore. Register this function's URL as a webhook endpoint in Stripe Dashboard > Developers > Webhooks. Listen for the 'checkout.session.completed' event. Always verify the Stripe signature using your webhook signing secret (different from your API secret key) to prevent fake payment events. Once the signature is verified, create an order document in Firestore with status 'paid', the user ID from session.metadata.userId, the amount, and the Stripe session ID for reference.

**Expected result:** After completing a test payment, a new order document appears in your Firestore 'orders' collection with status 'paid' and the correct user ID.

### 6. Test with Stripe Test Cards and Go Live

Test your full payment flow using Stripe's test card numbers. In the Stripe Checkout page, enter card number 4242 4242 4242 4242, any future expiry date, any 3-digit CVC, and any postal code. This simulates a successful payment. Test a declined card with 4000 0000 0000 0002. After testing confirms the order flow works end-to-end, go live: activate your Stripe account (complete the account verification), get your live API keys from Stripe Dashboard, update your Firebase config: 'firebase functions:config:set stripe.secret_key=sk_live_xxx', update your webhook with the live signing secret, and redeploy your Cloud Functions.

**Expected result:** End-to-end payment works: user taps Buy, enters test card, payment succeeds, order document created in Firestore, user sees payment confirmation in app.

## Complete code example

File: `stripe_webhook_orders.js`

```javascript
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const Stripe = require('stripe');

// Ensure admin is initialized
if (!admin.apps.length) admin.initializeApp();

exports.stripeWebhookOrderCreate = functions.https.onRequest(async (req, res) => {
  const stripe = Stripe(functions.config().stripe.secret_key);
  const webhookSecret = functions.config().stripe.webhook_secret;
  const sig = req.headers['stripe-signature'];

  let event;
  try {
    event = stripe.webhooks.constructEvent(req.rawBody, sig, webhookSecret);
  } catch (err) {
    console.error('Webhook signature failed:', err.message);
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  if (event.type === 'checkout.session.completed') {
    const session = event.data.object;
    const db = admin.firestore();

    try {
      await db.collection('orders').add({
        userId: session.metadata.userId || null,
        stripeSessionId: session.id,
        stripePaymentIntentId: session.payment_intent || null,
        amount: session.amount_total ? session.amount_total / 100 : 0,
        currency: session.currency || 'usd',
        status: 'paid',
        customerEmail: session.customer_details?.email || null,
        createdAt: admin.firestore.FieldValue.serverTimestamp(),
        paidAt: admin.firestore.FieldValue.serverTimestamp(),
      });
      console.log('Order created for session:', session.id);
    } catch (dbError) {
      console.error('Firestore write failed:', dbError);
      return res.status(500).json({ error: 'Failed to create order' });
    }
  }

  res.json({ received: true });
});
```

## Common mistakes

- **Putting the Stripe secret key (sk_test or sk_live) directly in a Custom Action** — Custom Action Dart code is compiled into your app binary. Anyone who downloads your app and decompiles it can extract the secret key. With your secret key, an attacker can create refunds, access customer data, and make charges to any card. Fix: Store the Stripe secret key only as a Firebase Functions config variable. Your Cloud Function uses it server-side where it is never exposed to users or app binaries.
- **Using URL parameters on the success redirect to confirm payment** — Stripe appends a session_id parameter to your success URL, but users can manually construct this URL with any session ID to fake a success page — without actually paying. Fix: Always confirm payment via a Stripe webhook event (checkout.session.completed). The webhook is server-to-server and cannot be faked without the webhook signing secret.
- **Not testing declined payments and cancellation flows** — Most builders test the happy path (successful payment) and skip testing failures. Users experience declined cards regularly and your app needs to handle these gracefully with clear messages. Fix: Test with Stripe's decline test card (4000 0000 0000 0002) and verify your app shows a meaningful error. Test closing the Stripe Checkout tab without paying and verify the cart remains available.

## Best practices

- Never expose your Stripe secret key in client-side code — always use a Cloud Function intermediary
- Always verify Stripe webhook signatures before processing any payment event
- Store the Stripe session ID and payment intent ID in every Firestore order document for debugging
- Test your full payment flow with all Stripe test cards (success, decline, insufficient funds) before going live
- Show a loading indicator while the checkout URL is being generated — the Cloud Function call takes 1-3 seconds
- Use Stripe's test mode webhook sending to verify your webhook handler works before enabling live mode
- Display clear payment confirmation within your app (not just relying on Stripe's success page) by checking the order document status

## Frequently asked questions

### Can I add Stripe payments to FlutterFlow without any code?

Not for secure production payments. Stripe requires a server-side component to create Checkout Sessions safely — this is a Firebase Cloud Function. The Cloud Function requires JavaScript code. However, the FlutterFlow side uses a Custom Action (Dart code provided above) which you can copy-paste without modification, and FlutterFlow's Launch URL action handles opening the Stripe Checkout page with zero custom code.

### Which Stripe pricing model works best with FlutterFlow?

Stripe Checkout (hosted payment page) is the easiest integration — your app simply opens the Stripe URL. This works for one-time payments and subscriptions. Stripe Payment Links (no code at all, just a URL) work if you only need a single fixed product. For custom payment forms inside your app, Stripe Elements requires more advanced Custom Widget code.

### How do I test Stripe payments in FlutterFlow without real money?

Use Stripe test mode keys (they start with sk_test_ and pk_test_). On the Stripe Checkout page, enter test card 4242 4242 4242 4242 with any future date and any CVC. No real money is charged. Switch to live keys only when you're ready to accept real payments from real users.

### Does Stripe work with FlutterFlow mobile apps (iOS and Android)?

Yes. The Stripe Checkout Session approach (Cloud Function + Launch URL) opens the checkout in the device's default browser, which works on all platforms. For an in-app payment experience without leaving the app, you would need the Stripe Flutter SDK integrated as a Custom Widget, which is more complex.

### How do I handle subscription payments vs one-time payments?

Change the 'mode' parameter in your Cloud Function from 'payment' to 'subscription'. Create a recurring price in your Stripe product (instead of a one-time price). The checkout flow is identical for users. For subscriptions, listen for the invoice.paid webhook event (not just checkout.session.completed) to track ongoing billing status.

### What happens if my Firebase Cloud Function goes down during a payment?

If the Cloud Function fails before creating the Checkout Session, the user sees an error in your app and no charge occurs — Stripe hasn't been called yet. If the payment succeeds but your webhook Cloud Function fails, the order won't be created in Firestore. Stripe retries failed webhooks automatically for 72 hours, so a temporary function outage self-recovers. Check your Firebase Function logs for any persistent webhook failures.

---

Source: https://www.rapidevelopers.com/flutterflow-tutorials/how-to-add-payments-to-your-flutterflow-project
© RapidDev — https://www.rapidevelopers.com/flutterflow-tutorials/how-to-add-payments-to-your-flutterflow-project
