Most FlutterFlow payment problems fall into six categories: Stripe test/live key mismatch, CORS errors from calling Stripe on the client, webhook signature failures, card_declined test card confusion, blank checkout redirects, and double charges from non-idempotent webhook handlers. The key insight is that payments happen in Cloud Functions and Stripe — not in FlutterFlow itself — so that is where you debug.
Why FlutterFlow Payment Debugging Is Different
When a payment fails in your FlutterFlow app the error rarely comes from FlutterFlow itself. Stripe processes the charge, Firebase Cloud Functions handle the server-side logic, and FCM or Firestore carries the result back to the UI. Checking FlutterFlow's Run Mode logs will show you the API call failed, but not why. You need three tabs open simultaneously: Stripe Dashboard > Logs, Firebase Console > Functions > Logs, and Firebase Console > Functions > Health. This guide walks through each common failure category, shows you exactly where to look, and gives you the fix.
Prerequisites
- Stripe account with API keys (publishable and secret)
- Firebase Cloud Function handling the Stripe charge or Checkout session creation
- Webhook endpoint configured in Stripe Dashboard pointing to your Cloud Function URL
- FlutterFlow project calling the Cloud Function via a custom API call or Cloud Function action
Step-by-step guide
Check Stripe Dashboard logs before touching FlutterFlow
Check Stripe Dashboard logs before touching FlutterFlow
Open Stripe Dashboard > Developers > Logs. Every API call Stripe receives is listed here with status code, request body, and response. If your charge attempt does not appear at all, the problem is upstream — your Cloud Function never called Stripe, likely due to a CORS error or a function crash before the Stripe call. If it appears with a 402 or 400 error, read the error.code field in the response — it will be one of the strings listed below. Filter by your test API key (starts with sk_test) vs live key (sk_live) to make sure you are looking at the right environment.
Expected result: You can see whether the charge request reached Stripe and what error code was returned.
Fix CORS errors by moving Stripe calls to a Cloud Function
Fix CORS errors by moving Stripe calls to a Cloud Function
If you see a CORS or network error in your Flutter debug console when tapping the pay button, you are calling the Stripe API directly from the app. Flutter (like a browser) cannot call stripe.com from client code with a secret key — and doing so would also expose your secret key in the app binary. The fix is to create a Firebase Cloud Function that accepts the charge parameters, calls Stripe server-side, and returns the result. In FlutterFlow, update your payment action to call the Cloud Function instead of Stripe directly. Pass only the amount and currency from the client; keep the secret key in the function's environment variables.
1// functions/createPaymentIntent.js2const { onCall } = require('firebase-functions/v2/https');3const Stripe = require('stripe');45exports.createPaymentIntent = onCall(async (request) => {6 const stripe = Stripe(process.env.STRIPE_SECRET_KEY);7 const { amount, currency } = request.data;8 const paymentIntent = await stripe.paymentIntents.create({9 amount, // in smallest currency unit, e.g. cents10 currency,11 automatic_payment_methods: { enabled: true },12 });13 return { clientSecret: paymentIntent.client_secret };14});Expected result: The CORS error disappears. The Cloud Function returns a client secret your app uses to confirm the payment.
Resolve test vs live mode mismatches
Resolve test vs live mode mismatches
One of the most common silent failures is using a test publishable key on the client but a live secret key in the Cloud Function, or vice versa. Stripe keys are environment-specific and cannot be mixed. Publishable keys start with pk_test_ or pk_live_; secret keys with sk_test_ or sk_live_. In FlutterFlow check your API call headers or custom action where you pass the publishable key. In Firebase Console check the Cloud Function's environment variable. Both must use the same prefix (test or live). Also verify that your Stripe webhook endpoint's signing secret matches the environment — Stripe issues separate webhook secrets for test and live mode.
Expected result: Keys are consistent across client, function, and webhook. Charges appear in the correct Stripe Dashboard section.
Fix webhook signature verification failures
Fix webhook signature verification failures
If your Cloud Function receives the webhook event but returns a 400 error, open Firebase Functions logs and look for 'No signatures found matching the expected signature for payload' or 'Webhook signature verification failed.' This happens for three reasons: the wrong webhook signing secret is set in the function's environment, the request body has been parsed as JSON before verification (signature requires the raw bytes), or the timestamp is more than 5 minutes off (clock skew). Fix: ensure your Cloud Function reads the raw body using express.raw({ type: '*/*' }) before the verification step, and paste the correct webhook secret from Stripe Dashboard > Developers > Webhooks > your endpoint > Signing secret.
1// Correct webhook handler — preserve raw body2const express = require('express');3const { onRequest } = require('firebase-functions/v2/https');4const Stripe = require('stripe');56const app = express();7app.post('/webhook', express.raw({ type: '*/*' }), (req, res) => {8 const stripe = Stripe(process.env.STRIPE_SECRET_KEY);9 const sig = req.headers['stripe-signature'];10 let event;11 try {12 event = stripe.webhooks.constructEvent(13 req.body, sig, process.env.STRIPE_WEBHOOK_SECRET14 );15 } catch (err) {16 return res.status(400).send(`Webhook Error: ${err.message}`);17 }18 if (event.type === 'payment_intent.succeeded') {19 // update Firestore order status20 }21 res.json({ received: true });22});23exports.stripeWebhook = onRequest(app);Expected result: Webhook events are verified and processed. Orders update in Firestore after successful payment.
Prevent double charges with idempotency keys
Prevent double charges with idempotency keys
If users are being charged twice, the likely cause is that your webhook handler processes payment_intent.succeeded events multiple times. Stripe can deliver the same event more than once if your endpoint returns a non-2xx status or times out. Fix this by making your handler idempotent: check whether the order has already been marked paid in Firestore before processing. Use the Stripe event ID as the Firestore document ID for processed events, and use a Firestore transaction to set status to 'paid' only if it is currently 'pending'. Also pass an idempotency key when creating the PaymentIntent so Stripe deduplicates server-side retries.
1// Idempotent webhook handler2if (event.type === 'payment_intent.succeeded') {3 const pi = event.data.object;4 const db = getFirestore();5 const eventRef = db.collection('processed_webhook_events').doc(event.id);6 await db.runTransaction(async (t) => {7 const eventDoc = await t.get(eventRef);8 if (eventDoc.exists) return; // already processed9 t.set(eventRef, { processedAt: FieldValue.serverTimestamp() });10 const orderRef = db.collection('orders').doc(pi.metadata.orderId);11 t.update(orderRef, { status: 'paid', paidAt: FieldValue.serverTimestamp() });12 });13}Expected result: Each payment_intent.succeeded event is processed exactly once, even if Stripe delivers it multiple times.
Complete working example
1const { onCall, onRequest } = require('firebase-functions/v2/https');2const { getFirestore, FieldValue } = require('firebase-admin/firestore');3const { initializeApp } = require('firebase-admin/app');4const Stripe = require('stripe');5const express = require('express');67initializeApp();89// Create PaymentIntent — called from FlutterFlow10exports.createPaymentIntent = onCall(async (request) => {11 const stripe = Stripe(process.env.STRIPE_SECRET_KEY);12 const { amount, currency, orderId } = request.data;13 const paymentIntent = await stripe.paymentIntents.create({14 amount,15 currency: currency || 'usd',16 automatic_payment_methods: { enabled: true },17 metadata: { orderId, userId: request.auth?.uid },18 // Idempotency: pass the orderId so retries reuse the same intent19 }, { idempotencyKey: orderId });20 return { clientSecret: paymentIntent.client_secret };21});2223// Webhook handler — receives Stripe events24const webhookApp = express();25webhookApp.post('/', express.raw({ type: '*/*' }), async (req, res) => {26 const stripe = Stripe(process.env.STRIPE_SECRET_KEY);27 let event;28 try {29 event = stripe.webhooks.constructEvent(30 req.body,31 req.headers['stripe-signature'],32 process.env.STRIPE_WEBHOOK_SECRET33 );34 } catch (err) {35 return res.status(400).send(`Webhook Error: ${err.message}`);36 }37 if (event.type === 'payment_intent.succeeded') {38 const pi = event.data.object;39 const db = getFirestore();40 const eventRef = db.collection('processed_webhook_events').doc(event.id);41 await db.runTransaction(async (t) => {42 const snap = await t.get(eventRef);43 if (snap.exists) return;44 t.set(eventRef, { processedAt: FieldValue.serverTimestamp() });45 if (pi.metadata?.orderId) {46 t.update(db.collection('orders').doc(pi.metadata.orderId), {47 status: 'paid',48 paidAt: FieldValue.serverTimestamp(),49 stripePaymentIntentId: pi.id,50 });51 }52 });53 }54 res.json({ received: true });55});56exports.stripeWebhook = onRequest(webhookApp);Common mistakes when troubleshooting Common FlutterFlow Payments Problems
Why it's a problem: Debugging payment issues by looking only at FlutterFlow logs
How to avoid: Open Stripe Dashboard > Developers > Logs and Firebase Console > Functions > Logs simultaneously. These are your primary debugging tools for payment failures.
Why it's a problem: Using test card 4242 4242 4242 4242 in live mode
How to avoid: Verify you are in test mode (Stripe Dashboard shows 'Test mode' banner in orange) when using test cards. Switch to live mode only with real card details.
Why it's a problem: Not setting a minimum charge amount
How to avoid: Validate the charge amount on the client before calling the Cloud Function, and add a server-side check in the function that rejects amounts below 50.
Why it's a problem: Parsing the webhook request body as JSON before signature verification
How to avoid: Use express.raw({ type: '*/*' }) on the webhook route so req.body contains the raw Buffer, not a parsed object.
Why it's a problem: Forgetting to add the webhook endpoint to Stripe Dashboard
How to avoid: After every deployment, verify the webhook URL in Stripe Dashboard > Developers > Webhooks matches your current Cloud Function URL.
Best practices
- Keep Stripe secret keys exclusively in Cloud Function environment secrets — never in FlutterFlow API configuration or app code.
- Always use Stripe's test card set (4242, 4000000000000002 for decline, 4000002760003184 for 3DS) rather than guessing error scenarios.
- Log every payment attempt to a Firestore payments collection with timestamp, amount, status, and Stripe PaymentIntent ID for reconciliation.
- Implement idempotency keys on both the PaymentIntent creation and the webhook handler to prevent duplicate charges.
- Set up Stripe radar rules to automatically block suspicious payments before they reach your app logic.
- Use Stripe's built-in Checkout page for your first integration — it handles 3DS, Apple Pay, and Google Pay automatically without custom code.
- Monitor your Cloud Function error rate in Firebase Console > Functions > Health and set up alerting for spikes after deployments.
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I have a FlutterFlow app with Stripe payments via Firebase Cloud Functions. My webhook is not updating Firestore after successful payments. Walk me through diagnosing the issue — where should I look in Stripe Dashboard and Firebase Console, and what are the most common causes of webhook handler failures?
Write a Firebase Cloud Function in Node.js that creates a Stripe PaymentIntent, handles the payment_intent.succeeded webhook with raw body preservation and signature verification, and updates a Firestore orders collection idempotently using the Stripe event ID as a deduplication key.
Frequently asked questions
Why does my payment work in Run Mode but fail after deployment?
The most common cause is environment variable differences. Run Mode in FlutterFlow uses your test configuration, while the deployed app may be missing the Stripe publishable key in its build environment. Check that all required keys are set in Firebase Function secrets and in FlutterFlow's production API configuration.
What does 'card_declined' with no decline_code mean?
A generic decline without a sub-code means Stripe received a decline from the card network without a specific reason — often a soft decline. In test mode, use card 4000000000000002 to simulate a generic decline. In live mode, ask the customer to try a different card or contact their bank.
How do I test webhooks locally without deploying Cloud Functions?
Install the Stripe CLI and run 'stripe listen --forward-to http://localhost:5001/YOUR-PROJECT/us-central1/stripeWebhook'. This tunnels Stripe events to your local emulator so you can debug without deploying.
My Stripe Checkout shows a blank white screen. What is wrong?
A blank Checkout page usually means the session creation failed or returned an invalid URL. Check Firebase Functions logs — the most common causes are a missing or incorrect Stripe key, an amount below the minimum, or a currency mismatch between the session and your Stripe account's supported currencies.
Can I add Apple Pay and Google Pay to my FlutterFlow app?
Yes, through Stripe. Use Stripe's Payment Request Button in a WebView, or integrate the flutter_stripe package via custom code export. Both Apple Pay and Google Pay are supported in Stripe Checkout automatically with no extra code.
Why are customers being charged twice?
Double charges almost always come from non-idempotent webhook handlers. Stripe retries webhook delivery if your endpoint returns a non-2xx status. If your handler processes the event each time, orders get updated (or charged) on every retry. Fix by storing processed event IDs in Firestore and checking before processing.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation