# How to Create a Custom Payment Widget for Your FlutterFlow App

- Tool: FlutterFlow
- Difficulty: Beginner
- Time required: 30-40 min
- Compatibility: FlutterFlow Pro+ (Cloud Functions required)
- Last updated: March 2026

## TL;DR

Integrate Stripe payments by creating a Cloud Function that generates a Stripe Checkout Session URL from cart items, then redirecting users to Stripe's hosted page via Launch URL. Build an order summary Component showing line items, subtotal, tax, and total before redirecting. Handle payment confirmation via a Stripe webhook Cloud Function that updates the orders collection in Firestore. Never put Stripe secret keys in client-side code.

## Integrating Stripe Checkout Payments in FlutterFlow

Every monetized app needs payment processing. Stripe Checkout is the safest and fastest integration — Stripe handles the payment form, PCI compliance, and receipts. Your FlutterFlow app creates a session via Cloud Function and redirects users to Stripe's hosted page.

## Before you start

- FlutterFlow Pro plan or higher
- A Stripe account with API keys (test mode)
- A Cloud Function environment (Firebase Functions or Supabase Edge Functions)
- A Firestore orders collection

## Step-by-step guide

### 1. Set up Stripe API keys securely in Cloud Function environment variables

In your Cloud Function environment (Firebase or Supabase), add STRIPE_SECRET_KEY as an environment variable with your test mode secret key (sk_test_...). Never put this key in FlutterFlow API headers, Custom Code, or anywhere in client-side code. Also add STRIPE_WEBHOOK_SECRET for webhook signature verification. The publishable key (pk_test_...) can be used client-side if needed for Stripe Elements, but we use Checkout which does not need it.

**Expected result:** Stripe secret key is stored securely in server-side environment variables only.

### 2. Create the Cloud Function to generate a Stripe Checkout Session

Write a Cloud Function (HTTP triggered) that receives cart items (array of name, price, quantity) and userId. The function creates a Stripe Checkout Session: stripe.checkout.sessions.create({ line_items: cartItems.map(item => ({price_data: {currency: 'usd', product_data: {name: item.name}, unit_amount: item.price * 100}, quantity: item.quantity})), mode: 'payment', success_url: 'https://yourapp.com/order-confirmed?session_id={CHECKOUT_SESSION_ID}', cancel_url: 'https://yourapp.com/checkout', metadata: {userId} }). Return the session.url to the client.

```
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

exports.createCheckoutSession = async (req, res) => {
  const { items, userId } = req.body;
  const session = await stripe.checkout.sessions.create({
    line_items: items.map(item => ({
      price_data: {
        currency: 'usd',
        product_data: { name: item.name },
        unit_amount: Math.round(item.price * 100),
      },
      quantity: item.quantity,
    })),
    mode: 'payment',
    success_url: `https://yourapp.com/order-confirmed?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: 'https://yourapp.com/checkout',
    metadata: { userId },
  });
  res.json({ url: session.url });
};
```

**Expected result:** The Cloud Function returns a Stripe Checkout URL when called with cart items.

### 3. Build the order summary page with line items and total

Create a CheckoutPage. Display cart items in a ListView: each Row shows item name, quantity, and price. Below the list, add a Divider, then Rows for Subtotal (sum of item.price * quantity), Tax (subtotal * tax rate), and Total (subtotal + tax) in bold. Use a Custom Function to calculate these values from the cart items App State array. Add a 'Proceed to Payment' button at the bottom.

**Expected result:** An order summary showing all cart items with calculated subtotal, tax, and total.

### 4. Call the Cloud Function and redirect to Stripe Checkout

On the Proceed to Payment button tap, add an API Call action targeting your Cloud Function URL. Pass the cart items and userId in the request body. On success, extract the session.url from the response. Add a Launch URL action with the checkout URL. Stripe opens in the device browser where the user completes payment. On success, Stripe redirects to your success_url.

**Expected result:** Tapping the button calls the Cloud Function and redirects the user to Stripe's hosted checkout page.

### 5. Handle payment confirmation with a Stripe webhook Cloud Function

Create a second Cloud Function for the webhook endpoint. Stripe sends a POST to this URL on events like checkout.session.completed. Verify the webhook signature using stripe.webhooks.constructEvent(body, sig, STRIPE_WEBHOOK_SECRET). On verified checkout.session.completed event, extract the session metadata (userId) and create/update a document in the orders collection: userId, items, amount, paymentStatus: 'paid', stripeSessionId, paidAt. Register the webhook URL in Stripe Dashboard → Webhooks.

**Expected result:** Stripe notifies your Cloud Function when payment completes, which updates the Firestore orders collection.

## Complete code example

File: `create_checkout_session.js`

```dart
// Cloud Function: Create Stripe Checkout Session
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const admin = require('firebase-admin');
admin.initializeApp();

exports.createCheckoutSession = async (req, res) => {
  try {
    const { items, userId } = req.body;

    if (!items || !items.length) {
      return res.status(400).json({ error: 'No items provided' });
    }

    const session = await stripe.checkout.sessions.create({
      line_items: items.map(item => ({
        price_data: {
          currency: 'usd',
          product_data: { name: item.name },
          unit_amount: Math.round(item.price * 100),
        },
        quantity: item.quantity,
      })),
      mode: 'payment',
      success_url: `https://yourapp.com/confirmed?sid={CHECKOUT_SESSION_ID}`,
      cancel_url: 'https://yourapp.com/checkout',
      metadata: { userId },
    });

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

// Webhook Handler
exports.stripeWebhook = async (req, res) => {
  const sig = req.headers['stripe-signature'];
  let event;
  try {
    event = stripe.webhooks.constructEvent(
      req.rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET
    );
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  if (event.type === 'checkout.session.completed') {
    const session = event.data.object;
    await admin.firestore().collection('orders').add({
      userId: session.metadata.userId,
      amount: session.amount_total / 100,
      currency: session.currency,
      paymentStatus: 'paid',
      stripeSessionId: session.id,
      paidAt: admin.firestore.FieldValue.serverTimestamp(),
    });
  }
  res.json({ received: true });
};
```

## Common mistakes

- **Putting the Stripe secret key in FlutterFlow API headers or Custom Code** — Client-side code is visible to anyone who decompiles your app. Exposed secret keys let attackers make charges, issue refunds, and access your Stripe account. Fix: Only store the Stripe secret key in Cloud Function environment variables. Client-side code should never see or transmit it.
- **Not verifying the webhook signature in the payment handler** — Without signature verification, anyone can send fake payment events to your webhook URL and trick your app into marking orders as paid. Fix: Always verify the signature using stripe.webhooks.constructEvent() with the STRIPE_WEBHOOK_SECRET. Reject unverified events.
- **Calculating the payment amount on the client side** — Users can modify client-side price calculations to pay less. The server should be the source of truth for pricing. Fix: Calculate the final amount in the Cloud Function by looking up prices from your database, not by trusting client-sent prices.

## Best practices

- Never expose Stripe secret keys in client-side code — use Cloud Functions only
- Always verify webhook signatures before processing payment events
- Use test mode keys (sk_test_) during development and switch to live keys for production
- Calculate prices server-side to prevent client-side manipulation
- Store order data in Firestore with payment status for order management
- Handle both success and cancel redirect URLs in the Checkout Session
- Test with Stripe test card numbers (4242 4242 4242 4242) before going live

## Frequently asked questions

### Can I embed Stripe's payment form directly in my app?

It is technically possible using Stripe Elements via a WebView Custom Widget, but Stripe Checkout (hosted page) is strongly recommended. It handles PCI compliance, 3D Secure, and all payment methods without you building a form.

### How do I handle subscription payments?

Change the Checkout Session mode from 'payment' to 'subscription' and use Stripe Price IDs instead of inline price_data. Stripe handles recurring billing automatically.

### What happens if the user closes the browser during payment?

The Checkout Session expires after 24 hours. No charge occurs. The cancel_url redirect only triggers if the user clicks 'Back'. Use webhooks as the source of truth for payment status, not redirect URLs.

### Can I use PayPal instead of Stripe?

Stripe Checkout supports PayPal as a payment method. Enable it in your Stripe Dashboard under Payment Methods. Users see both card and PayPal options on the checkout page.

### How do I issue refunds?

Use the Stripe Dashboard for manual refunds, or create a Cloud Function calling stripe.refunds.create({payment_intent: 'pi_xxx'}) for programmatic refunds triggered from your app.

### Can RapidDev help with payment integration?

Yes. RapidDev can implement Stripe Checkout, subscription billing, webhook handlers, refund flows, and revenue dashboards for your FlutterFlow app.

---

Source: https://www.rapidevelopers.com/flutterflow-tutorials/how-to-create-a-custom-payment-widget-for-your-flutterflow-app
© RapidDev — https://www.rapidevelopers.com/flutterflow-tutorials/how-to-create-a-custom-payment-widget-for-your-flutterflow-app
