Skip to main content
RapidDev - Software Development Agency
flutterflow-integrationsCustom Action (Dart)

Stripe Terminal

Connect FlutterFlow to Stripe Terminal using a Dart Custom Action that wraps the stripe_terminal pub.dev SDK for physical card readers. A Firebase or Supabase proxy mints short-lived connection tokens using your secret key. This is a native-only integration — it will not run in FlutterFlow's web preview canvas; test on a physical iOS or Android device paired with a real reader.

What you'll learn

  • Why Stripe Terminal requires a native Custom Action in FlutterFlow and will not work in web preview
  • How to deploy a connection-token endpoint in Firebase or Supabase to keep your secret key server-side
  • How to configure iOS Bluetooth and location permissions in FlutterFlow's platform settings so reader discovery works
  • How to build Custom Actions for the full Terminal flow: discover → connect → collectPaymentMethod → processPayment
  • How to surface reader connection state and payment results through App State variables and stream them to the UI
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced15 min read3-5 hoursPaymentsLast updated July 2026RapidDev Engineering Team
TL;DR

Connect FlutterFlow to Stripe Terminal using a Dart Custom Action that wraps the stripe_terminal pub.dev SDK for physical card readers. A Firebase or Supabase proxy mints short-lived connection tokens using your secret key. This is a native-only integration — it will not run in FlutterFlow's web preview canvas; test on a physical iOS or Android device paired with a real reader.

Quick facts about this guide
FactValue
ToolStripe Terminal
CategoryPayments
MethodCustom Action (Dart)
DifficultyAdvanced
Time required3-5 hours
Last updatedJuly 2026

Stripe Terminal is a native hardware SDK — here is what that means for FlutterFlow

When most people hear 'Stripe', they picture online card forms. Stripe Terminal is a completely different product: it accepts in-person payments through a physical card reader (the BBPOS WisePad 3, Stripe Reader S700, and others) via Bluetooth or USB. The reader handles the tap/chip/swipe, encrypts the payment data on the hardware, and sends it to Stripe. This is the setup you'd use for a retail point-of-sale, a pop-up market checkout, or a mobile services business collecting payment on-site.

Because Terminal communicates with hardware over Bluetooth and uses a proprietary SDK for payment method collection, there is no REST-only path — you cannot replicate it with FlutterFlow's standard API Calls panel. The integration lives entirely in a Dart Custom Action wrapping the stripe_terminal pub.dev package. This means two important things for FlutterFlow developers: first, the Custom Action will not run in FlutterFlow's web Test Mode or the canvas preview — you must test on a physical iOS or Android device. Second, iOS requires explicit Bluetooth and Location permission strings in Info.plist, and Android requires Bluetooth and location runtime permissions. Skipping these means the reader discovery function silently returns zero readers with no error.

Stripe Terminal pricing is separate from online Stripe: in-person transactions in the US are charged at approximately 2.7% + 5¢ per transaction (verify current rates on Stripe's pricing page). Reader hardware (WisePad 3, S700) is purchased separately from the Stripe Dashboard. The Terminal SDK communicates with Stripe's servers to process payments, so a live internet connection on the mobile device is required even for hardware-based acceptance.

Integration method

Custom Action (Dart)

FlutterFlow cannot integrate Stripe Terminal through REST API Calls alone because Terminal requires a native SDK that manages the Bluetooth/USB connection to a physical card reader, orchestrates the payment flow on-device, and handles cryptographic payment method collection. A Dart Custom Action wrapping the stripe_terminal pub.dev package is the only viable path. Separately, a Firebase Cloud Function or Supabase Edge Function proxies the connection-token endpoint so your secret key never enters the Flutter bundle.

Prerequisites

  • A Stripe account with Terminal enabled (Dashboard → Terminal → Get started); order at least one compatible reader (BBPOS WisePad 3 or Stripe Reader S700) from the Stripe hardware shop
  • A Firebase project (Blaze plan) or Supabase project to host the connection-token Cloud Function or Edge Function
  • A FlutterFlow project targeting iOS and/or Android (Terminal is mobile-only; web is not supported)
  • Your Stripe secret key (sk_*) stored in your backend environment — never in FlutterFlow
  • A physical iOS or Android device for testing — the Terminal Custom Action will not run in the FlutterFlow canvas or Run Mode

Step-by-step guide

1

Enable Terminal in the Stripe Dashboard and order a reader

Log in to dashboard.stripe.com and navigate to Terminal → Get started. If you haven't registered a reader location yet, create one (Terminal → Locations → + New location). Each physical reader must be assigned to a Stripe Terminal location. From the Terminal hardware shop (hardware.stripe.com), order a reader compatible with your use case. The BBPOS WisePad 3 connects over Bluetooth and is the most common choice for mobile POS on iOS and Android. The Stripe Reader S700 is a smart reader with a built-in touchscreen, better for semi-fixed countertop setups. Both accept chip, tap, and swipe. Note your reader's serial number — you'll pair it in a later step. Also navigate to Developers → API keys and securely copy your secret key (sk_test_* for development, sk_live_* for production). This key will go only into your backend proxy — not into FlutterFlow.

Pro tip: Order the BBPOS WisePad 3 for most FlutterFlow mobile POS builds — it uses Bluetooth LE and has broad iOS and Android support. The S700 requires Ethernet or Wi-Fi and is better for fixed locations.

Expected result: Terminal is enabled in your Stripe Dashboard, a reader location is created, and you have a reader (or are waiting for one to arrive). Your secret key is saved securely outside FlutterFlow.

2

Deploy a connection-token endpoint as a Firebase or Supabase function

Stripe Terminal's SDK requires a fresh connection token before it can communicate with any reader. The SDK calls a token-provider function you supply during initialization — this function must call POST https://api.stripe.com/v1/terminal/connection_tokens using your secret key and return the token string. Because the secret key must never be in the Flutter app, you deploy this as a Firebase Cloud Function or Supabase Edge Function. For Firebase (Node.js): create a function that receives a POST request from the app, calls the Stripe connection_tokens endpoint with your sk_* key from environment config, and returns { secret: token.secret }. Register the function's URL in your FlutterFlow project as a Dart constant (not in an API Call group — you'll pass it directly into the Terminal SDK initialization in the Custom Action). For Supabase (Deno): create an Edge Function at supabase/functions/connection-token/index.ts that does the same thing, deployed with supabase functions deploy connection-token and the secret set via supabase secrets set STRIPE_SECRET_KEY=sk_xxx. The URL pattern will be https://yourproject.supabase.co/functions/v1/connection-token.

index.js
1// Firebase Cloud Function: connection token endpoint (Node.js)
2const functions = require('firebase-functions');
3const stripe = require('stripe')(functions.config().stripe.secret_key);
4const cors = require('cors')({ origin: true });
5
6exports.connectionToken = functions.https.onRequest((req, res) => {
7 cors(req, res, async () => {
8 try {
9 const token = await stripe.terminal.connectionTokens.create();
10 res.json({ secret: token.secret });
11 } catch (err) {
12 res.status(500).json({ error: err.message });
13 }
14 });
15});
16
17// Deploy with:
18// firebase functions:config:set stripe.secret_key="sk_test_xxx"
19// firebase deploy --only functions:connectionToken

Pro tip: Connection tokens are short-lived (~15 minutes). The SDK will call your token provider again automatically when a token expires, so you don't need to cache them — just return a fresh one on every request.

Expected result: The connection-token endpoint is deployed and publicly accessible. Calling it with a POST request returns JSON like { "secret": "wrcsec_xxx" }.

3

Add the stripe_terminal package and configure platform permissions

In FlutterFlow, go to Custom Code in the left navigation panel → + Add → Action. Name the action first one TerminalInit. Before writing any Dart, click into the Dependencies field and type stripe_terminal, select the package (use version ^2.1.0 or later — verify the current stable version on pub.dev before adding). FlutterFlow will automatically add this to the generated pubspec.yaml when building. The stripe_terminal SDK communicates with physical readers over Bluetooth LE, which on iOS requires both Bluetooth and Location permissions (iOS requires location access for Bluetooth LE scanner). On Android, Bluetooth and location runtime permissions are needed. In FlutterFlow, go to Settings → App Details → Permissions (iOS) and enable 'Bluetooth Always' and 'Location When In Use' — add descriptive usage strings like 'This app uses Bluetooth to connect to your card reader' and 'Location is needed to discover nearby card readers'. For Android, go to Settings → App Details → Permissions (Android) and enable Bluetooth, Bluetooth Scan, Bluetooth Connect, and Access Fine Location. Missing even one of these strings causes reader discovery to return zero results with no error message in the console — it's the silent failure that trips up almost every first-time Terminal developer.

custom_action.dart
1// Verify exact version on pub.dev before adding:
2// https://pub.dev/packages/stripe_terminal
3// Package name to add in FlutterFlow Custom Code → Dependencies:
4// stripe_terminal
5//
6// Also add this guard at the top of every Terminal Custom Action:
7import 'package:flutter/foundation.dart' show kIsWeb;
8
9if (kIsWeb) {
10 // Terminal is not supported on web
11 // Show a user-friendly message or return early
12 return;
13}

Pro tip: Add the kIsWeb guard at the top of every Terminal Custom Action. Without it, a web build will crash when the SDK tries to call native Bluetooth APIs that don't exist in the browser.

Expected result: The stripe_terminal dependency is listed in Custom Code → Dependencies. iOS permission strings appear in the platform settings. Android manifest permissions are enabled. FlutterFlow's Run/Build shows no dependency errors.

4

Build Custom Actions for the full Terminal payment flow

You need four Dart Custom Actions that mirror the Terminal SDK's state machine. Create each under Custom Code → + Add → Action. Action 1 — TerminalInit: initialize the Terminal SDK with your connection-token provider URL. The SDK calls this URL every time it needs a fresh token. Action 2 — DiscoverReaders: call Terminal.instance.discoverReaders() with your desired discovery method (bluetoothScan or bluetoothProximity for WisePad 3). Collect the list of discovered readers into an App State variable (List of String, containing reader serial numbers or display names) and show them in a FlutterFlow ListView. Action 3 — ConnectReader: accept a reader serial from the list, call Terminal.instance.connectBluetoothReader() with that reader and a locationId (the Stripe Terminal location ID you created). Store the connected reader state in an App State variable. Action 4 — CollectAndProcessPayment: create a PaymentIntent amount (in cents), call Terminal.instance.collectPaymentMethod(), then Terminal.instance.processPayment(). On success, extract the PaymentIntent ID from the result and write it to Firestore or Supabase as a confirmed transaction. Wire these four actions to buttons in a FlutterFlow POS screen: 'Search for Readers' → DiscoverReaders, reader list item tap → ConnectReader, 'Charge Card' button → CollectAndProcessPayment. Use App State boolean variables (isConnecting, isProcessing) to show loading spinners while each step runs.

custom_action.dart
1// Action 1: TerminalInit
2import 'package:flutter/foundation.dart' show kIsWeb;
3import 'package:stripe_terminal/stripe_terminal.dart';
4
5Future terminalInit(String connectionTokenUrl) async {
6 if (kIsWeb) return;
7 await StripeTerminal.instance.init(
8 fetchToken: () async {
9 final response = await http.post(Uri.parse(connectionTokenUrl));
10 final body = jsonDecode(response.body);
11 return body['secret'] as String;
12 },
13 );
14}
15
16// Action 4: CollectAndProcessPayment
17import 'package:stripe_terminal/stripe_terminal.dart';
18
19Future<String> collectAndProcessPayment(int amountInCents) async {
20 if (kIsWeb) return '';
21 // Create PaymentIntent on Stripe via your backend, then collect
22 final paymentIntent = await StripeTerminal.instance.createPaymentIntent(
23 CreatePaymentIntentParameters(
24 amount: amountInCents,
25 currency: 'usd',
26 paymentMethodTypes: [PaymentMethodType.cardPresent],
27 ),
28 );
29 final collected = await StripeTerminal.instance
30 .collectPaymentMethod(paymentIntent.id);
31 final processed = await StripeTerminal.instance.processPayment(collected.id);
32 return processed.id; // PaymentIntent ID
33}

Pro tip: Reader discovery can take 10-30 seconds over Bluetooth. Set an App State 'isDiscovering' boolean to true while DiscoverReaders runs and show a progress indicator. Clear it when the reader list is populated or a timeout occurs.

Expected result: The four Custom Actions appear in FlutterFlow's Custom Code panel. Wired to the POS screen, 'Search for Readers' triggers Bluetooth scanning and populates a list. Tapping a reader connects it. 'Charge Card' presents the reader to the customer and processes payment.

5

Test on a physical device and handle edge cases

Because the Terminal Custom Actions use native Bluetooth APIs, you must test on a physical iOS or Android device — the FlutterFlow canvas preview, Run Mode, and web exports will not execute these actions. Use FlutterFlow's 'Test on Device' feature (top right → Test on Device → scan QR with your device) or export the Flutter project and run it from your IDE. Pair your reader with the Stripe test environment first: in the Stripe Dashboard (test mode), go to Terminal → Readers and register your reader's serial number to your test location. Use Stripe's test card 4242 4242 4242 4242 in test mode — your physical WisePad 3 or S700 will accept it without charging real money. Watch your App State variables in FlutterFlow's Debug Mode panel to trace each step. Common edge cases to handle: (a) reader goes to sleep after ~2 minutes of inactivity — add a 'Reconnect Reader' button that calls ConnectReader again; (b) payment collection is cancelled by the customer (card removed) — wrap collectPaymentMethod in a try/catch and surface an error state in App State; (c) network loss mid-payment — the SDK holds the payment method data locally until connectivity is restored, but surface a 'No internet' warning to the operator. If you'd rather skip building the permission configuration and error-state management yourself, RapidDev's team builds FlutterFlow Terminal integrations like this every week — free scoping call at rapidevelopers.com/contact.

Pro tip: In the Stripe Dashboard → Terminal → Readers, switch your test reader to 'Simulated' mode for initial SDK testing — you don't need a physical reader until the full flow is wired up. The simulated reader appears in discovery results on device and accepts test cards.

Expected result: On a physical device, the app discovers the paired reader, connects to it, presents the reader to a customer for tap/chip/swipe, and records a successful PaymentIntent in Firestore or Supabase. The Stripe Dashboard shows the test transaction.

Common use cases

Mobile food truck point-of-sale app

A vendor uses a FlutterFlow app on their phone to display a menu, add items to a cart, and collect payment with a BBPOS WisePad 3 over Bluetooth. The app shows a running order history and daily totals pulled from Firestore.

FlutterFlow Prompt

Build a POS screen that shows menu items, lets the vendor tap to add them, displays the total, and triggers a Stripe Terminal card reader payment. After payment succeeds, show a receipt screen.

Copy this prompt to try it in FlutterFlow

On-site services booking and payment app

A home cleaning company's technician uses the FlutterFlow app to view their assigned jobs, mark services complete, and collect payment from the customer right at the door using a paired card reader. Payment confirmation emails to the customer fire via Firestore-triggered Cloud Function.

FlutterFlow Prompt

Show the technician their job list. When they tap 'Collect Payment', open the Stripe Terminal flow to charge the customer's card. On success, update the job status to 'paid' and show a confirmation.

Copy this prompt to try it in FlutterFlow

Pop-up retail checkout with inventory sync

A fashion brand sells at pop-up markets using a FlutterFlow tablet app. Staff scan or tap product barcodes, the app looks up the item and price from a Supabase inventory table, and Terminal handles card acceptance. Stock levels decrement on each sale.

FlutterFlow Prompt

Create a checkout flow where staff enter a product SKU, see the price from our database, and charge the customer's card via a paired Terminal reader. Decrement inventory by 1 on each successful payment.

Copy this prompt to try it in FlutterFlow

Troubleshooting

Reader discovery returns zero readers with no error message

Cause: Bluetooth or location permissions are missing from iOS Info.plist or Android manifest, or the user has not granted runtime permissions. The SDK silently returns an empty list when it cannot scan.

Solution: In FlutterFlow's Settings → App Details → Permissions, confirm that Bluetooth Always and Location When In Use (iOS) and Bluetooth Scan, Bluetooth Connect, and Access Fine Location (Android) are all enabled with descriptive usage strings. Run the app on device and check that the OS permission prompt appears and is accepted by the user. Also confirm the reader is in pairing mode (flashing LED on WisePad 3).

Terminal Custom Action does nothing in FlutterFlow's canvas or Run Mode

Cause: Custom Actions that wrap native SDKs (including stripe_terminal) do not execute in FlutterFlow's web-based Test Mode or canvas preview — they are compiled Flutter code that only runs in a native app build.

Solution: Use FlutterFlow's 'Test on Device' option (top right of the canvas) or export the project and build an APK/IPA. The action will execute correctly on device even if it shows nothing in the canvas. Add a kIsWeb guard at the top of each Terminal action to prevent crashes in web builds.

typescript
1import 'package:flutter/foundation.dart' show kIsWeb;
2
3// At the top of every Terminal Custom Action:
4if (kIsWeb) {
5 // Show user-friendly message; Terminal is mobile-only
6 return;
7}

Connection token fetch returns 401 or the SDK logs 'invalid connection token'

Cause: The secret key used in your connection-token Cloud Function or Edge Function is wrong, expired, or from a different Stripe environment (e.g., live key used against test-mode readers).

Solution: Check your Firebase Functions config or Supabase secrets to confirm the sk_test_* or sk_live_* key is set correctly and matches the Stripe environment your readers are registered in. Also verify the connection-token endpoint URL stored in FlutterFlow matches the deployed function URL exactly.

collectPaymentMethod throws MissingPluginException on Android

Cause: The stripe_terminal pub.dev package has not been resolved into the native Android build — this can happen if FlutterFlow's dependency cache is stale.

Solution: In FlutterFlow, go to Custom Code, remove the stripe_terminal dependency, save, then add it back. Rebuild the test APK from scratch. If exporting the Flutter project manually, run flutter clean and flutter pub get before rebuilding.

Best practices

  • Always add a kIsWeb guard at the top of every Terminal Custom Action — Terminal is mobile-only, and without the guard a web build will crash trying to call native Bluetooth APIs.
  • Store the connection-token endpoint URL in FlutterFlow App Values (Settings → App Values), not hardcoded in the Dart action — this makes swapping between test and production endpoints easy without touching code.
  • Use App State boolean variables (isDiscovering, isConnecting, isProcessing) to track each step of the Terminal flow and show appropriate loading indicators — the Bluetooth and payment steps can each take several seconds.
  • Handle the reader-sleep scenario (readers sleep after ~2 minutes) with a 'Reconnect' button that calls your ConnectReader action; don't assume the reader stays connected between transactions.
  • Always pass amounts in cents (integer) to the Terminal SDK: $49.99 = 4999. A decimal amount like 49.99 will cause an SDK error or a wildly wrong charge.
  • Test with Stripe's simulated reader (registered in the Dashboard → Terminal → Readers as 'Simulated') before acquiring physical hardware — it runs on-device and accepts test card numbers without Bluetooth.
  • Register a webhook endpoint for payment_intent.payment_failed to catch reader-declined or card-error events; the Terminal SDK processPayment() call may return a failed state that needs explicit UI handling.
  • Keep the Terminal SDK version pinned in your Custom Code dependencies — the stripe_terminal pub.dev package has breaking changes between minor versions; check the changelog before upgrading.

Alternatives

Frequently asked questions

Does Stripe Terminal work on iPad or tablet FlutterFlow apps?

Yes — Stripe Terminal works on iOS iPads and Android tablets running your FlutterFlow app, as long as the device supports Bluetooth LE. The reader discovery and payment flow behave the same as on a phone. Larger screens give you more room for a multi-item cart layout, which many POS apps prefer.

Can I use Stripe Terminal on a FlutterFlow web app?

No. Stripe Terminal relies on native Bluetooth and USB APIs that browsers do not expose. There is no web SDK for Terminal. If you need in-browser payment acceptance, use the standard Stripe online payment flow (Stripe Elements or Payment Sheet) instead — that's completely separate from Terminal.

Do I need an internet connection on the device to accept Terminal payments?

Yes. The Terminal SDK communicates with Stripe's servers to authorise and process each payment, so the device running your FlutterFlow app must have an active internet connection (cellular or Wi-Fi). The reader hardware itself does not connect to the internet — it connects to the app over Bluetooth, and the app connects to Stripe.

What happens if the customer taps or inserts a card and then removes it before processing completes?

The SDK will throw an error from collectPaymentMethod() or processPayment(). Wrap those calls in a try/catch in your Custom Action, catch the StripeTerminalException, set an App State 'paymentError' variable with the message, and show a retry button on the POS screen. Common error codes include PAYMENT_DECLINED and COMMAND_NOT_ALLOWED.

How do I switch from Stripe test mode to live mode?

Update your connection-token Cloud Function or Edge Function to use the live secret key (sk_live_*) instead of the test key. Update any FlutterFlow App Values that store your publishable key to use pk_live_*. In the Stripe Dashboard, switch from Test mode to Live mode and re-register your physical readers under Live mode locations. Do not mix test and live credentials — using a test key with a live-mode reader (or vice versa) will result in authentication errors.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Integrations are where projects stall

We wire FlutterFlow integrations like this one every week — auth, webhooks, edge cases included. Fixed-price builds from $13K, delivered in 6–10 weeks.

Talk to an integration engineer

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.