# How to Implement a Peer-to-Peer Payment System in FlutterFlow

- Tool: FlutterFlow
- Difficulty: Beginner
- Time required: 30-40 min
- Compatibility: FlutterFlow Free+
- Last updated: March 2026

## TL;DR

Build a peer-to-peer payment system using Stripe Connect with Express accounts for each user. Cloud Functions handle transfers via stripe.transfers.create(), manage KYC verification through Stripe Connect onboarding, and track balances. Users send money by selecting a recipient and entering an amount, which triggers a Cloud Function that creates a Stripe transfer between connected accounts. Each user completes identity verification through Stripe's hosted onboarding flow before receiving payouts.

## Building Payment Infrastructure for User-to-User Transfers

Peer-to-peer payments require more than just charging a card. This tutorial covers the Stripe Connect infrastructure: creating Express accounts for each user, handling KYC identity verification, executing transfers between accounts, and tracking balances. Stripe handles the complex compliance, tax reporting, and payout logistics while your FlutterFlow app provides the user interface.

## Before you start

- A FlutterFlow project with Firestore and authentication configured
- A Stripe account with Connect enabled (Settings > Connect in Stripe Dashboard)
- Stripe API secret key stored in Cloud Function environment variables
- Cloud Functions environment configured for your project

## Step-by-step guide

### 1. Set up Stripe Connect and the Firestore data model

In the Stripe Dashboard, enable Connect and configure your platform settings. Choose Express account type for simplicity. In Firestore, extend user documents with: stripeConnectId (String, nullable), stripeOnboardingComplete (Boolean, default false), stripePayoutsEnabled (Boolean). Create a transfers collection: fromUserId (String), toUserId (String), amount (Integer, in cents), currency (String, 'usd'), stripeTransferId (String), status (String: 'pending', 'completed', 'failed'), createdAt (Timestamp), note (String, optional message from sender). This model tracks all transfers with their Stripe reference IDs for reconciliation.

**Expected result:** Stripe Connect is enabled and Firestore has user payment fields and a transfers collection for tracking all transactions.

### 2. Create Cloud Functions for Stripe Connect account onboarding

Build two Cloud Functions. First, createConnectAccount: creates a Stripe Express account for the user via stripe.accounts.create() with type 'express', stores the account ID in the user's stripeConnectId field, then generates an Account Link URL via stripe.accountLinks.create() with return and refresh URLs pointing to your app. Second, checkOnboardingStatus: checks the Stripe account's charges_enabled and payouts_enabled fields and updates the user's Firestore document accordingly. The user opens the Account Link URL in a WebView or external browser to complete Stripe's hosted identity verification form.

```
// Cloud Function: createConnectAccount
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const stripe = require('stripe')(
  process.env.STRIPE_SECRET_KEY);

exports.createConnectAccount = functions.https
  .onCall(async (data, context) => {
    const userId = context.auth.uid;
    const user = (await admin.firestore()
      .doc(`users/${userId}`).get()).data();
    // Create Express account
    const account = await stripe.accounts.create({
      type: 'express',
      email: user.email,
      capabilities: {
        transfers: { requested: true },
      },
    });
    await admin.firestore().doc(`users/${userId}`)
      .update({ stripeConnectId: account.id });
    // Create onboarding link
    const accountLink = await stripe.accountLinks
      .create({
        account: account.id,
        refresh_url: `${data.baseUrl}/onboarding-refresh`,
        return_url: `${data.baseUrl}/onboarding-complete`,
        type: 'account_onboarding',
      });
    return { url: accountLink.url, accountId: account.id };
  });

exports.checkOnboardingStatus = functions.https
  .onCall(async (data, context) => {
    const userId = context.auth.uid;
    const user = (await admin.firestore()
      .doc(`users/${userId}`).get()).data();
    const account = await stripe.accounts
      .retrieve(user.stripeConnectId);
    await admin.firestore().doc(`users/${userId}`)
      .update({
        stripeOnboardingComplete:
          account.details_submitted,
        stripePayoutsEnabled:
          account.payouts_enabled,
      });
    return {
      complete: account.details_submitted,
      payoutsEnabled: account.payouts_enabled
    };
  });
```

**Expected result:** Users create a Stripe Connect account and complete identity verification through Stripe's hosted onboarding flow.

### 3. Build the identity verification and wallet setup UI

Create a WalletSetupPage. Check the user's stripeOnboardingComplete field. If false, show a setup card explaining that identity verification is required to send and receive money. Add a Verify Identity button that calls the createConnectAccount Cloud Function and opens the returned URL in a WebView or launches it externally. When the user returns, call checkOnboardingStatus to update their status. If onboarding is complete, show a green checkmark with 'Identity Verified' status. Display a KYC status indicator: 'Not Started' (grey), 'In Progress' (yellow), 'Verified' (green), 'Action Required' (red, when Stripe needs more info).

**Expected result:** Users see their verification status and can complete Stripe's identity verification flow. Status updates reflect in real-time.

### 4. Implement the send money flow with Cloud Function transfers

Create a SendMoneyPage with a user search TextField to find the recipient by name or email. Display the selected recipient's name and avatar. Add an amount TextField (numeric, formatted as currency) and an optional note TextField. On send, call a transferMoney Cloud Function that: validates both sender and recipient have verified Stripe Connect accounts, creates a charge on the sender via stripe.charges.create() with the platform as destination, then creates a transfer to the recipient via stripe.transfers.create(). Write the transfer record to Firestore with status 'completed'. Show a success animation and transfer confirmation with details.

```
// Cloud Function: transferMoney
exports.transferMoney = functions.https
  .onCall(async (data, context) => {
    const { toUserId, amount, note } = data;
    const fromUserId = context.auth.uid;
    const db = admin.firestore();
    const fromUser = (await db.doc(
      `users/${fromUserId}`).get()).data();
    const toUser = (await db.doc(
      `users/${toUserId}`).get()).data();
    if (!fromUser.stripePayoutsEnabled ||
        !toUser.stripePayoutsEnabled) {
      throw new functions.https.HttpsError(
        'failed-precondition',
        'Both users must complete verification');
    }
    // Create transfer
    const transfer = await stripe.transfers.create({
      amount: amount, // in cents
      currency: 'usd',
      destination: toUser.stripeConnectId,
      transfer_group: `p2p_${fromUserId}_${toUserId}`,
    });
    // Record in Firestore
    await db.collection('transfers').add({
      fromUserId, toUserId, amount,
      currency: 'usd',
      stripeTransferId: transfer.id,
      status: 'completed',
      note: note || '',
      createdAt: admin.firestore
        .FieldValue.serverTimestamp()
    });
    return { success: true,
      transferId: transfer.id };
  });
```

**Expected result:** Users search for a recipient, enter an amount, and send money. The Cloud Function processes the Stripe transfer and records it in Firestore.

### 5. Display transfer history and available balance

Create a WalletPage showing the user's current balance and transfer history. Call a Cloud Function that retrieves the Stripe balance for the user's Connect account via stripe.balance.retrieve({stripeAccount: connectId}). Display the available balance prominently at the top. Below, add a ListView with a Backend Query on transfers where fromUserId equals current user OR toUserId equals current user, ordered by createdAt descending. Each transfer row shows: direction icon (up arrow for sent, down arrow for received), recipient or sender name, amount (red for sent, green for received), note text if present, date, and status badge. Add filter ChoiceChips for All, Sent, and Received.

**Expected result:** Users see their available balance and a filterable list of all sent and received transfers with amounts and dates.

## Complete code example

File: `FlutterFlow P2P Payment Setup`

```text
FIRESTORE DATA MODEL:
  users/{userId} (extended)
    stripeConnectId: String (nullable)
    stripeOnboardingComplete: Boolean
    stripePayoutsEnabled: Boolean

  transfers/{transferId}
    fromUserId: String
    toUserId: String
    amount: Integer (cents)
    currency: 'usd'
    stripeTransferId: String
    status: 'pending' | 'completed' | 'failed'
    note: String (optional)
    createdAt: Timestamp

CLOUD FUNCTIONS:
  1. createConnectAccount
     → stripe.accounts.create({ type: 'express' })
     → stripe.accountLinks.create() → return onboarding URL

  2. checkOnboardingStatus
     → stripe.accounts.retrieve(connectId)
     → update user doc with verification status

  3. transferMoney(toUserId, amount, note)
     → validate both users verified
     → stripe.transfers.create({ destination: connectId })
     → record in transfers collection

  4. getBalance
     → stripe.balance.retrieve({ stripeAccount: connectId })
     → return available balance

PAGE: WalletSetupPage
  Column
    ├── KYC Status Indicator (grey/yellow/green/red)
    ├── If not verified:
    │     Card: explanation + Verify Identity button
    │     → calls createConnectAccount → opens onboarding URL
    └── If verified:
          Text: 'Identity Verified' + green checkmark

PAGE: SendMoneyPage
  Column
    ├── TextField (search recipient by name/email)
    ├── Container (selected recipient: avatar + name)
    ├── TextField (amount, numeric, currency format)
    ├── TextField (optional note)
    └── Button: Send Money
          → Cloud Function transferMoney
          → success animation + confirmation

PAGE: WalletPage
  Column
    ├── Container (balance card: available amount)
    ├── ChoiceChips (All | Sent | Received)
    └── ListView (transfers, ordered by date desc)
          Row: direction icon + name + amount + note + date
```

## Common mistakes

- **Using regular Stripe charges and manual payouts instead of Stripe Connect** — Without Connect, you handle identity verification, tax reporting, payout scheduling, and compliance yourself. This creates massive accounting and legal overhead. Fix: Use Stripe Connect with Express accounts. Stripe handles KYC verification, identity checks, tax reporting (1099s), and payout scheduling automatically.
- **Allowing transfers before both users complete identity verification** — Unverified users could receive funds that Stripe then freezes, creating a poor experience. Stripe requires verification before enabling payouts. Fix: Check stripePayoutsEnabled for both sender and recipient before processing any transfer. Show clear status indicators and verification prompts.
- **Storing Stripe API keys in FlutterFlow frontend code or Firestore** — The Stripe secret key can create charges and transfers. Exposing it allows anyone to make unauthorized transactions on your Stripe account. Fix: Store the Stripe secret key only in Cloud Function environment variables. All Stripe API calls happen server-side in Cloud Functions, never from the client.

## Best practices

- Use Stripe Connect Express accounts so Stripe handles KYC, compliance, and payouts
- Process all Stripe API calls through Cloud Functions, never from the client
- Verify both sender and recipient have completed onboarding before allowing transfers
- Store transfer records in Firestore for your own audit trail alongside Stripe's records
- Display KYC status prominently with clear calls to action for incomplete verification
- Use amounts in cents (integers) to avoid floating-point rounding errors
- Implement webhook listeners for transfer status updates from Stripe

## Frequently asked questions

### What is Stripe Connect and why do I need it for P2P payments?

Stripe Connect is Stripe's platform for enabling payments between users. It handles identity verification, compliance, tax reporting, and payouts for each user. Without it, you would need to build all of this infrastructure yourself, which is legally complex and time-consuming.

### How long does Stripe identity verification take?

Most verifications complete instantly or within minutes. Some cases requiring manual document review can take 1-2 business days. Users can check their status in the app and Stripe sends email updates.

### What are the fees for P2P transfers using Stripe Connect?

Stripe charges 2.9% + 30 cents per transaction for card payments, plus 0.25% + 25 cents per payout to connected accounts. You can choose whether the sender, recipient, or your platform absorbs these fees.

### Can users withdraw their balance to a bank account?

Yes. Stripe Connect Express accounts include automatic payout scheduling. Users set their bank account during onboarding and Stripe sends payouts on a configurable schedule (daily, weekly, monthly).

### How do I handle transfer disputes or refunds?

Create a dispute flow where users can flag a transfer. An admin reviews disputes and can initiate a refund via stripe.refunds.create() in a Cloud Function. Stripe also has built-in dispute handling for card chargebacks.

### Can RapidDev help build a payment platform with advanced features?

Yes. RapidDev can implement split payments, escrow holds, multi-currency transfers, recurring payments, group expense splitting, and detailed financial reporting dashboards.

---

Source: https://www.rapidevelopers.com/flutterflow-tutorials/how-to-implement-a-peer-to-peer-payment-system-in-flutterflow
© RapidDev — https://www.rapidevelopers.com/flutterflow-tutorials/how-to-implement-a-peer-to-peer-payment-system-in-flutterflow
