Connect FlutterFlow to Plaid using a Dart Custom Action that wraps the plaid_flutter pub.dev SDK to launch the Plaid Link account-connection UI. A Firebase or Supabase proxy handles all token operations: creating the link_token, exchanging the user's public_token for a permanent access_token, and calling transaction or balance endpoints. The access_token and secret never touch the Flutter app.
| Fact | Value |
|---|---|
| Tool | Plaid |
| Category | Payments |
| Method | Custom Action (Dart) |
| Difficulty | Advanced |
| Time required | 3-4 hours |
| Last updated | July 2026 |
Plaid reads bank data — it does not move money
The most important thing to understand about Plaid before building: it is not a payment processor. It does not charge cards, send money, or process transactions. Plaid is a bank data aggregator — it lets your app read a user's account balances, transaction history, income, and identity information from thousands of banks and credit unions, with the user's explicit consent. The use cases are fintech apps, personal finance dashboards, income verification for lending, budgeting tools, and KYC flows.
The integration has a mandatory native piece called Plaid Link — a consent-and-authentication screen where the user selects their bank, enters their banking credentials on Plaid's secure overlay, and approves access. Plaid Link runs as a native iOS/Android UI via the plaid_flutter pub.dev SDK. There is no web version of this SDK that works in Flutter, which means the Custom Action will not open in FlutterFlow's web Test Mode or canvas preview. You must test on a real device.
The token dance is the technical heart of the integration and all of it must be server-side: your backend calls Plaid's /link/token/create endpoint with your client_id and secret to generate a short-lived link_token for a specific user. The app opens Link with this link_token. When the user completes linking, Plaid returns a temporary public_token to the app. The app immediately forwards this public_token to your backend, which calls /item/public_token/exchange to convert it into a permanent access_token. That access_token is stored server-side, associated with the user — it's what your backend uses for all future balance and transaction calls. The client never sees the access_token. Plaid's sandbox environment (sandbox.plaid.com) is free and provides test institutions and test credentials you can use to develop the full flow without connecting real bank accounts.
Integration method
Plaid has two integration layers: the Link UI (a native consent screen where the user connects their bank) and the Data API (which reads balances and transactions). The Link UI requires the plaid_flutter pub.dev SDK, which only runs in native iOS and Android builds — not in FlutterFlow's web preview. A Dart Custom Action launches Link with a link_token from your backend. After the user completes the flow, the public_token is sent to the backend for exchange. All token creation, exchange, and data calls happen in a Firebase or Supabase proxy that holds the Plaid secret.
Prerequisites
- A Plaid account at dashboard.plaid.com — start with the Sandbox environment (free, no approval needed)
- Your Plaid client_id and secret from the Plaid Dashboard → Team Settings → Keys (note the environment-specific secrets: sandbox, development, production)
- A Firebase project (Blaze plan) or Supabase project for the backend proxy that will hold the secret and access_tokens
- A FlutterFlow project targeting iOS and/or Android — Plaid Link does not work in web builds
- A physical iOS or Android device for testing — the plaid_flutter Custom Action will not run in the FlutterFlow canvas preview
Step-by-step guide
Create a Plaid account and collect your sandbox credentials
Go to dashboard.plaid.com and sign up for a free Plaid account. You'll start in the Sandbox environment automatically — no application or approval is needed. Navigate to Team Settings → Keys. You'll see three sets of credentials corresponding to Sandbox, Development, and Production environments, each with a client_id and a secret. For now, copy your Sandbox client_id and your Sandbox secret. Store them somewhere secure (you'll add them to Firebase Functions config or Supabase secrets). Note that each environment uses a different base URL: Sandbox uses https://sandbox.plaid.com, Development uses https://development.plaid.com, and Production uses https://production.plaid.com. Using credentials from one environment against the wrong host returns a 400 or authentication error — this is one of the most common setup mistakes. In the Plaid Dashboard, you can also enable specific products (Transactions, Auth, Identity, Income) and configure which institutions to show in Plaid Link (sandbox supports a set of test institutions including 'First Platypus Bank'). You do not need to configure FlutterFlow yet — all credentials stay on the server.
Pro tip: The Plaid Dashboard has a 'Quickstart' section that lets you run through the full Link → token exchange → data call flow in your browser using sandbox credentials. Complete this before building the FlutterFlow integration — it gives you a clear picture of the full flow and lets you verify your credentials work.
Expected result: You have Plaid sandbox client_id and secret copied securely. You've tested credentials in the Plaid Quickstart and confirmed they work for the Sandbox environment.
Deploy a backend proxy for link_token creation and public_token exchange
Your proxy needs to handle at least three endpoints. Endpoint 1 — POST /create-link-token: receives the current user's ID from FlutterFlow, calls POST https://sandbox.plaid.com/link/token/create with your client_id, secret, a user object containing client_user_id (your internal user ID), client_name (your app name), products (e.g. ['transactions', 'auth']), country_codes (['US']), and language ('en'). Returns the link_token string to the app — this token is short-lived (30 minutes). Endpoint 2 — POST /exchange-public-token: receives the public_token returned by Plaid Link after the user connects their bank. Calls POST https://sandbox.plaid.com/item/public_token/exchange with client_id, secret, and public_token. Receives the permanent access_token and item_id. Stores the access_token in Firestore (collection 'plaid_items', document keyed by user ID) or Supabase (plaid_items table) — never return it to the app. Returns { success: true } to the app. Endpoint 3 — POST /get-balances (or /get-transactions): receives the user's ID, looks up their access_token from Firestore/Supabase, calls POST https://sandbox.plaid.com/accounts/balance/get with client_id, secret, and access_token, and returns the formatted balances to the app. All three endpoints read the client_id and secret from environment config — not from request parameters.
1// Firebase Cloud Function: Plaid proxy (Node.js)2const functions = require('firebase-functions');3const axios = require('axios');4const admin = require('firebase-admin');5const cors = require('cors')({ origin: true });6const express = require('express');7const app = express();8app.use(cors);9app.use(express.json());10admin.initializeApp();1112const PLAID_BASE = 'https://sandbox.plaid.com'; // switch env as needed13const PLAID_CLIENT_ID = () => functions.config().plaid.client_id;14const PLAID_SECRET = () => functions.config().plaid.secret;1516// 1. Create link token17app.post('/create-link-token', async (req, res) => {18 const { user_id } = req.body;19 const response = await axios.post(`${PLAID_BASE}/link/token/create`, {20 client_id: PLAID_CLIENT_ID(),21 secret: PLAID_SECRET(),22 user: { client_user_id: user_id },23 client_name: 'My Finance App',24 products: ['transactions'],25 country_codes: ['US'],26 language: 'en',27 });28 res.json({ link_token: response.data.link_token });29});3031// 2. Exchange public token32app.post('/exchange-public-token', async (req, res) => {33 const { public_token, user_id } = req.body;34 const response = await axios.post(`${PLAID_BASE}/item/public_token/exchange`, {35 client_id: PLAID_CLIENT_ID(),36 secret: PLAID_SECRET(),37 public_token,38 });39 await admin.firestore().collection('plaid_items').doc(user_id).set({40 access_token: response.data.access_token,41 item_id: response.data.item_id,42 updated_at: new Date(),43 });44 res.json({ success: true });45});4647// 3. Get balances48app.post('/get-balances', async (req, res) => {49 const { user_id } = req.body;50 const doc = await admin.firestore().collection('plaid_items').doc(user_id).get();51 const { access_token } = doc.data();52 const response = await axios.post(`${PLAID_BASE}/accounts/balance/get`, {53 client_id: PLAID_CLIENT_ID(),54 secret: PLAID_SECRET(),55 access_token,56 });57 res.json({ accounts: response.data.accounts });58});5960exports.plaid = functions.https.onRequest(app);Pro tip: Store access_tokens in Firestore with strict security rules that only allow your Cloud Function's service account to read them — never allow client-side reads of the plaid_items collection. This prevents any user from reading another user's banking access.
Expected result: Three proxy endpoints are deployed. Calling /create-link-token with a user_id returns a link_token string. After completing the Plaid Quickstart flow manually, calling /exchange-public-token stores the access_token in Firestore and returns { success: true }. Calling /get-balances returns account data.
Add the plaid_flutter Custom Action with a kIsWeb guard
In FlutterFlow, go to Custom Code in the left navigation panel → + Add → Action. Name this action OpenPlaidLink. Before writing Dart, click into the Dependencies field, type plaid_flutter, and add the package (check pub.dev for the current stable version, e.g. ^4.0.0). FlutterFlow adds this to the Flutter project's pubspec.yaml automatically. Now write the Custom Action. It will: (1) call your proxy's /create-link-token endpoint with the current user's ID to get a link_token; (2) open Plaid Link using the PlaidLink.open() method from the SDK with the link_token; (3) in the onSuccess callback, call your proxy's /exchange-public-token endpoint with the received public_token and the user's ID; (4) on success, update an App State boolean (bankLinked: true) and navigate the user to the account dashboard. Add a kIsWeb guard at the very top of the action so a web build doesn't crash attempting to call native APIs. Add two parameters to the action: proxyBaseUrl (String) and userId (String). In the Action Flow Editor on the 'Link Bank Account' button, add this Custom Action and wire proxyBaseUrl to your App Value (proxy URL) and userId to the current authenticated user ID.
1// Custom Action: OpenPlaidLink (plaid_flutter ^4.0.0)2import 'package:flutter/foundation.dart' show kIsWeb;3import 'package:plaid_flutter/plaid_flutter.dart';4import 'package:http/http.dart' as http;5import 'dart:convert';67Future<bool> openPlaidLink(8 String proxyBaseUrl,9 String userId,10) async {11 if (kIsWeb) {12 // Plaid Link is not supported on web13 return false;14 }1516 // Step 1: Get link token from backend17 final tokenResponse = await http.post(18 Uri.parse('$proxyBaseUrl/create-link-token'),19 headers: {'Content-Type': 'application/json'},20 body: jsonEncode({'user_id': userId}),21 );22 final linkToken = jsonDecode(tokenResponse.body)['link_token'] as String;2324 // Step 2: Open Plaid Link25 bool success = false;26 final config = LinkTokenConfiguration(token: linkToken);27 PlaidLink.open(configuration: config);2829 PlaidLink.onSuccess.listen((LinkSuccess event) async {30 final publicToken = event.publicToken;31 // Step 3: Exchange public token server-side32 await http.post(33 Uri.parse('$proxyBaseUrl/exchange-public-token'),34 headers: {'Content-Type': 'application/json'},35 body: jsonEncode({'public_token': publicToken, 'user_id': userId}),36 );37 success = true;38 });3940 return success;41}Pro tip: The kIsWeb guard is critical. Without it, a web export build will compile but crash at runtime when plaid_flutter tries to call native iOS/Android channel methods that don't exist in the browser.
Expected result: The OpenPlaidLink Custom Action is created in FlutterFlow and wired to the 'Link Bank Account' button. On a physical device, tapping the button opens the Plaid Link UI where users can select and authenticate with their bank.
Fetch balances and transactions from your proxy and display them
Once a user has linked their bank account (bankLinked App State = true), the app can display their financial data. In FlutterFlow, create API Calls in the left nav → API Calls panel that hit your proxy's /get-balances and /get-transactions endpoints. For GetBalances: Method POST, add a Body variable userId (String), parse the response using JSON Paths — the accounts array response includes account name, current balance, available balance, and account type. Create a FlutterFlow Data Type called PlaidAccount with fields: name (String), current_balance (Double), available_balance (Double), account_type (String). Use the API Call in a Backend Query on a ListView widget, binding each list item to a PlaidAccount. For GetTransactions: similarly, create a PlaidTransaction Data Type with fields: name (String), amount (Double), date (String), category (String). Bind a second ListView to the transactions data. On the Account Overview page, trigger both API Calls in the Page Load action (Action Flow Editor → On Page Load → Add API Call actions). Show loading indicators with a conditional visibility widget bound to an App State boolean (isLoadingData) that you set to true before the calls and false after. Format currency values using FlutterFlow's built-in number formatter or a formula: '$' + balance.toStringAsFixed(2).
1// Sample API responses for FlutterFlow Response & Test tab:23// GetBalances response:4{5 "accounts": [6 {7 "account_id": "acc_abc123",8 "name": "Checking Account",9 "type": "depository",10 "subtype": "checking",11 "balances": {12 "current": 2345.67,13 "available": 2100.0014 }15 }16 ]17}1819// JSON Paths to bind in FlutterFlow:20// Account name: $.accounts[0].name21// Current balance: $.accounts[0].balances.current22// Available balance: $.accounts[0].balances.availablePro tip: If you'd rather skip the token-exchange flow and data-binding complexity, RapidDev's team builds FlutterFlow financial data integrations like this every week — free scoping call at rapidevelopers.com/contact.
Expected result: After linking a bank account, the Account Overview page loads and shows the user's account name, current balance, and available balance from Plaid sandbox. Transaction history appears in a scrollable list below.
Test in Plaid sandbox and plan the production upgrade path
Plaid's sandbox provides test institutions and credentials you can use without connecting real bank accounts. In the Plaid Link UI (on your physical device), search for 'First Platypus Bank' — it's a test institution always available in sandbox. Use the credentials user_good / pass_good to complete the Link flow successfully. The sandbox returns realistic fake account and transaction data. Test the error paths too: use user_error / pass_error to simulate a failed authentication. Verify in your Firestore that the access_token is stored after a successful link, and that GetBalances returns data. For transactions, use /transactions/sync rather than /transactions/get for new integrations — sync is Plaid's recommended API and handles incremental updates. When you're ready to move beyond sandbox: Plaid's Development environment allows up to 100 real bank connections for testing with actual institutions (requires business information). Production requires Plaid approval, a privacy policy, and agreement to Plaid's terms — apply from the Plaid Dashboard under your team's settings. Change the proxy's base URL environment variable from sandbox.plaid.com to production.plaid.com and update the secret to the Production secret. Do not keep sandbox secrets in a production deployment.
1// Plaid sandbox test credentials:2// Institution: First Platypus Bank3// Username: user_good4// Password: pass_good5// MFA code (if prompted): 123467// Sandbox base URL: https://sandbox.plaid.com8// Development base URL: https://development.plaid.com9// Production base URL: https://production.plaid.com1011// Switch in your Firebase config:12// firebase functions:config:set plaid.env="production" plaid.secret="production_secret_here"Pro tip: Use Plaid's /transactions/sync endpoint instead of /transactions/get for transaction data — it supports incremental updates (only fetching new transactions since the last sync), which is more efficient and is the current Plaid recommendation.
Expected result: In sandbox, a full user journey completes: 'Link Bank Account' opens Plaid Link → user selects First Platypus Bank → enters test credentials → Plaid Link closes → balance and transaction screens populate with sandbox data. Firestore shows the item stored. Error paths (wrong credentials) show the Link UI's built-in error messages.
Common use cases
Personal finance app that shows spending by category
Users connect their bank accounts via Plaid Link in the FlutterFlow app. The app calls the backend to fetch transaction history and displays spending by category (food, transport, entertainment) in a Recharts-style bar chart. Users can set monthly budgets per category and receive Firebase push alerts when they approach the limit.
Let users connect their bank account, then show a screen with their last 30 days of transactions grouped by category. Highlight categories where spending exceeds their set budget.
Copy this prompt to try it in FlutterFlow
Income verification screen for a lending or rental app
Applicants link their primary bank account to prove income without uploading pay stubs. The backend calls Plaid's Income Insights product and returns a summary (average monthly deposit amount, employer name) displayed on the application review screen. The data is stored in Supabase alongside the application record.
Add a 'Verify your income' step to our loan application flow. Users connect their bank via Plaid, and we show the reviewing admin their average monthly income pulled from bank deposits.
Copy this prompt to try it in FlutterFlow
Net worth tracker that aggregates multiple bank and investment accounts
Users can connect multiple accounts (checking, savings, investment) via separate Plaid Link sessions. The FlutterFlow app shows a net worth screen that sums all account balances in real time, with a sparkline history of total balance over the past 90 days. Balance data is refreshed from the backend on app open.
Build a net worth dashboard where users can add multiple bank and investment accounts. Show total balance updated daily and a chart of balance history over the past 3 months.
Copy this prompt to try it in FlutterFlow
Troubleshooting
Plaid Link opens but immediately closes with no bank list or shows a blank screen
Cause: The link_token is invalid, expired (they expire after 30 minutes), or was generated for a different environment than the Plaid Link SDK is expecting.
Solution: Confirm that your /create-link-token proxy endpoint is returning a non-empty link_token. Add logging to the proxy to print the full Plaid API response. Ensure the base URL in your proxy (sandbox.plaid.com, development.plaid.com, or production.plaid.com) matches the environment of the client_id and secret you are using. Tokens expire after 30 minutes — generate a fresh link_token immediately before opening Plaid Link, not at page load.
Plaid Link Custom Action does nothing in FlutterFlow's canvas or Run Mode
Cause: The plaid_flutter SDK is a native plugin that only executes in compiled iOS and Android builds. It cannot run in FlutterFlow's browser-based canvas or web export.
Solution: Use FlutterFlow's 'Test on Device' option (top right → Test on Device → scan the QR code with your phone) to run the app on a physical device. The Custom Action will execute correctly on device. Always include a kIsWeb guard and a user-friendly fallback message for web builds.
1import 'package:flutter/foundation.dart' show kIsWeb;23if (kIsWeb) {4 // Show message: bank linking is only available on the mobile app5 return false;6}Exchange-public-token endpoint returns 400 INVALID_PUBLIC_TOKEN
Cause: The public_token returned by Plaid Link has already been exchanged (public tokens are single-use), or it was allowed to expire (they expire after 30 minutes).
Solution: Exchange the public_token immediately in the Plaid Link onSuccess callback — do not store it in App State or delay the exchange. Each public_token can only be exchanged once. If the exchange fails, the user needs to re-link their account (open Plaid Link again for a fresh public_token).
get-balances proxy returns 400 INVALID_ACCESS_TOKEN or 'No such access token'
Cause: The access_token stored in Firestore or Supabase is wrong, from a different environment (sandbox token used against production endpoint), or the user revoked access from their bank's OAuth consent screen.
Solution: Verify that the access_token in Firestore was stored using the same environment (sandbox vs production) as the proxy's current base URL. If the user revoked access, you'll need to re-launch Plaid Link for that user (create a new link_token with update_mode for the existing item, or start a fresh link). Log Plaid's full error response in the proxy for diagnosis.
Best practices
- Never return the Plaid access_token to the FlutterFlow app — store it server-side in Firestore or Supabase and always look it up in the backend before making any Plaid data calls.
- Generate a fresh link_token immediately before opening Plaid Link — tokens expire after 30 minutes, so generating them at page load risks expiry if the user delays.
- Exchange the public_token in the Plaid Link onSuccess callback immediately — public tokens are single-use and expire after 30 minutes.
- Use the sandbox environment and test credentials (user_good / pass_good at First Platypus Bank) for all development and QA before applying for Plaid production access.
- Store Plaid access_tokens in Firestore with security rules that block client-side reads — only your Cloud Function service account should have read access to the plaid_items collection.
- Add a kIsWeb guard to every plaid_flutter Custom Action — the SDK is native-only, and a missing guard will crash web builds.
- Use Plaid's /transactions/sync endpoint (not the deprecated /transactions/get) for fetching transaction data — it supports incremental updates and is Plaid's current recommendation.
- Handle the case where a user's bank connection breaks (item enters an error state, common with OAuth institutions after token expiry) — detect ITEM_LOGIN_REQUIRED errors from your data calls and re-launch Plaid Link in update mode for the existing item.
Alternatives
Braintree is a card payment processor (charging cards, PayPal checkout) — use it when you want to accept money, not when you need to read bank balance and transaction data like Plaid does.
Stripe Connect handles marketplace payment splits at checkout — it moves money between accounts, which is the opposite of Plaid's read-only bank data aggregation.
Adyen is an enterprise payment processor for card acceptance, not a bank data aggregation tool — the two serve entirely different use cases.
Frequently asked questions
Is Plaid a payment processor? Can it charge users' bank accounts?
No. Plaid is a read-only bank data aggregation platform (balances, transactions, income, identity). It does not charge accounts, move money, or initiate transfers. If you need to move funds from a user's bank account, you would combine Plaid (to verify the account) with an ACH processor like Stripe ACH or Dwolla. Plaid is the 'read the bank data' layer, not the 'move the money' layer.
Can Plaid Link work on a FlutterFlow web app?
Not with the plaid_flutter native SDK. The plaid_flutter package uses iOS and Android platform channels that do not exist in a browser environment. For web support, Plaid provides a JavaScript Web SDK, but that cannot be called from Dart/Flutter. If you need web bank-linking, you'd need to embed Plaid Link via a WebView widget that loads Plaid's JavaScript implementation — but this is complex and outside the scope of a standard FlutterFlow build. Target native mobile for Plaid integrations.
How much does Plaid cost?
Plaid Sandbox is free with no time limit and no real bank connections. Development allows up to 100 real linked Items (bank connections) at no charge, for testing with real institutions. Production pricing is per-linked-Item and per-product (Transactions, Auth, Identity, Income) — check Plaid's current pricing page at plaid.com/pricing as rates vary and are typically negotiated for higher volumes. Budget for ongoing per-Item fees if your app links many accounts.
What happens if a user's bank requires multi-factor authentication during Plaid Link?
Plaid Link handles MFA natively as part of the UI flow — it will prompt the user for their MFA code (SMS, app-based, security questions) within the Link overlay. Your app does not need to handle this. The plaid_flutter SDK's onSuccess callback fires only after all authentication steps, including MFA, are completed successfully.
How do I handle a user wanting to disconnect their bank account?
Call Plaid's /item/remove endpoint server-side, passing the user's access_token and your credentials. This revokes the connection on Plaid's side. Also delete the access_token from your Firestore or Supabase record. In FlutterFlow, wire a 'Disconnect Bank' button to an API Call that hits a /remove-item endpoint on your proxy, which does both operations. Set the bankLinked App State to false and navigate the user back to the 'Link Bank Account' screen.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation