FlutterFlow apps run on user devices and cannot receive webhooks — webhooks need a server always listening. Deploy a Cloud Function with an HTTPS onRequest trigger, register its URL as the webhook endpoint in Stripe, Shopify, or any external service. The Cloud Function verifies the webhook signature, processes the payload, and writes to Firestore. FlutterFlow reads the updated Firestore data via a real-time Backend Query.
Connect external service events to your FlutterFlow app via Cloud Function webhooks
Many developers try to register their FlutterFlow app's URL as a webhook endpoint in Stripe or Shopify — and discover it does not work. FlutterFlow apps run on user devices, not servers. A webhook needs a permanently running server to receive HTTP POST requests at any time. The solution is a Firebase Cloud Function: it provides a permanent HTTPS URL, runs in the cloud 24/7, and can write webhook data to Firestore where your FlutterFlow app picks it up via a real-time listener. This tutorial covers the full pattern: deploying the Cloud Function, verifying webhook signatures (critical for security), writing to Firestore, and building the FlutterFlow real-time UI that reacts to webhook events.
Prerequisites
- A Firebase project with Cloud Functions enabled (requires Blaze pay-as-you-go billing)
- An account with an external service that sends webhooks (Stripe, Shopify, SendGrid, GitHub, or similar)
- Node.js installed locally for writing and deploying Cloud Functions
- Basic understanding of FlutterFlow's Backend Query and real-time Firestore listeners
Step-by-step guide
Understand the webhook architecture — why your app cannot receive webhooks directly
Understand the webhook architecture — why your app cannot receive webhooks directly
A webhook is an HTTP POST request that an external service (like Stripe) sends to your server when an event happens — a payment succeeds, an order ships, a user unsubscribes. For this to work, your 'server' must have a permanent public URL that accepts HTTP requests at any moment. A FlutterFlow app runs on the user's iPhone or Android device — it does not have a public URL, it is not always running, and it sleeps when in the background. Even if it did have an IP address, the user's device changes networks constantly. The solution is a three-layer architecture: (1) External service sends webhook → (2) Firebase Cloud Function receives it, verifies the signature, and writes to Firestore → (3) FlutterFlow app reads from Firestore via a real-time listener and updates the UI. The Cloud Function is the server layer — it has a permanent HTTPS URL, is always available, and scales automatically. Firestore is the message bus — once the Cloud Function writes the event, all connected FlutterFlow app instances see the update within 100-300ms.
Expected result: You understand the three-layer pattern and are ready to implement each layer.
Deploy a Cloud Function HTTPS endpoint for webhook receipt
Deploy a Cloud Function HTTPS endpoint for webhook receipt
In your Firebase project, open the functions/index.js file (or create one with firebase init functions). Write an HTTPS onRequest function that accepts POST requests from your chosen external service. The function should: log the incoming request for debugging, verify the webhook signature (covered in the next step), parse the event type from the payload, write the relevant data to a Firestore collection, and return a 200 response quickly. Returning 200 is critical — external services retry webhooks if they do not receive a 200 within their timeout window (Stripe: 20 seconds, Shopify: 5 seconds). Process the actual work after you return 200, or use a Firestore write to queue work for a separate background function. Deploy with: firebase deploy --only functions. The deployed function URL appears in the Firebase Console → Functions → Dashboard. Copy this URL — it looks like https://us-central1-{project}.cloudfunctions.net/webhookHandler.
1// functions/index.js — Generic webhook receiver2const functions = require('firebase-functions');3const admin = require('firebase-admin');45admin.initializeApp();67exports.webhookHandler = functions.https.onRequest(async (req, res) => {8 // Always respond quickly — return 200 before heavy processing9 // External services retry if they don't get 200 within their timeout1011 if (req.method !== 'POST') {12 res.status(405).send('Method Not Allowed');13 return;14 }1516 const db = admin.firestore();1718 try {19 // Step 1: Verify signature (see next step for Stripe example)20 // const isValid = verifySignature(req);21 // if (!isValid) { res.status(401).send('Invalid signature'); return; }2223 // Step 2: Parse the event24 const event = req.body;25 const eventType = event.type || event.event || 'unknown';26 const eventId = event.id || db.collection('webhook_events').doc().id;2728 // Step 3: Prevent duplicate processing (idempotency)29 const existingDoc = await db.collection('webhook_events').doc(eventId).get();30 if (existingDoc.exists) {31 res.status(200).send('Already processed');32 return;33 }3435 // Step 4: Write to Firestore36 await db.collection('webhook_events').doc(eventId).set({37 eventType,38 payload: event,39 receivedAt: admin.firestore.FieldValue.serverTimestamp(),40 processed: false,41 });4243 // Step 5: Return 200 immediately44 res.status(200).json({ received: true });4546 // Step 6: Process the event AFTER returning (fire-and-forget)47 await processWebhookEvent(db, eventType, event);4849 } catch (err) {50 console.error('Webhook error:', err);51 // Return 200 even on processing errors to prevent retries52 // Log to a dead_letter_queue for manual inspection53 res.status(200).json({ received: true, processingError: err.message });54 }55});5657async function processWebhookEvent(db, eventType, event) {58 if (eventType === 'payment_intent.succeeded') {59 const pi = event.data?.object;60 await db.collection('payments').doc(pi.id).set({61 status: 'succeeded',62 amount: pi.amount,63 currency: pi.currency,64 customerId: pi.customer,65 updatedAt: admin.firestore.FieldValue.serverTimestamp(),66 });67 } else if (eventType === 'checkout.session.completed') {68 const session = event.data?.object;69 await db.collection('orders').doc(session.id).set({70 status: 'paid',71 customerEmail: session.customer_email,72 amountTotal: session.amount_total,73 updatedAt: admin.firestore.FieldValue.serverTimestamp(),74 });75 }76 // Add more event types as needed77}Expected result: Cloud Function is deployed with a permanent HTTPS URL. Sending a test POST to the URL returns 200 and creates a document in the webhook_events Firestore collection.
Verify the webhook signature to block spoofed requests
Verify the webhook signature to block spoofed requests
Without signature verification, anyone who discovers your Cloud Function URL can POST fake payment events, fake order completions, or any data they choose. Every serious webhook provider signs their payloads — Stripe uses HMAC-SHA256 with a signing secret, Shopify uses X-Shopify-Hmac-Sha256, GitHub uses X-Hub-Signature-256. For Stripe: get your webhook signing secret from Stripe Dashboard → Developers → Webhooks → your endpoint → Signing secret. Store it in Firebase config: firebase functions:config:set stripe.webhook_secret='whsec_YOUR_SECRET'. In the Cloud Function, use Stripe's official SDK to verify: the raw request body (not parsed JSON) must be passed to stripe.webhooks.constructEvent(). This is why you must use express.raw() middleware for the webhook route — normal JSON parsing corrupts the raw body that signature verification requires.
1// Stripe webhook signature verification2// Install: cd functions && npm install stripe3const stripe = require('stripe')(functions.config().stripe.secret_key);45exports.stripeWebhook = functions.https.onRequest(6 // CRITICAL: Use raw body for Stripe signature verification7 // In firebase.json, add rewrite or use express with raw body parser8 async (req, res) => {9 const sig = req.headers['stripe-signature'];10 const webhookSecret = functions.config().stripe.webhook_secret;1112 let event;13 try {14 // req.rawBody is the raw Buffer — required for HMAC verification15 // Firebase Functions automatically provides req.rawBody16 event = stripe.webhooks.constructEvent(17 req.rawBody,18 sig,19 webhookSecret20 );21 } catch (err) {22 console.error('Stripe signature verification failed:', err.message);23 res.status(400).send(`Webhook Error: ${err.message}`);24 return;25 }2627 // Signature verified — safe to process28 const db = admin.firestore();29 res.status(200).json({ received: true });3031 // Process after returning 20032 if (event.type === 'payment_intent.succeeded') {33 const paymentIntent = event.data.object;34 const userId = paymentIntent.metadata?.userId;3536 await db.collection('payments').doc(paymentIntent.id).set({37 userId,38 status: 'succeeded',39 amount: paymentIntent.amount / 100, // cents to dollars40 currency: paymentIntent.currency.toUpperCase(),41 createdAt: admin.firestore.FieldValue.serverTimestamp(),42 });4344 if (userId) {45 // Update user subscription status if applicable46 await db.collection('users').doc(userId).update({47 subscriptionStatus: 'active',48 lastPaymentAt: admin.firestore.FieldValue.serverTimestamp(),49 });50 }51 }52 }53);Expected result: Cloud Function rejects requests without a valid Stripe signature with HTTP 400, and accepts only genuinely Stripe-signed webhook events.
Register your Cloud Function URL as the webhook endpoint
Register your Cloud Function URL as the webhook endpoint
Copy your Cloud Function's HTTPS URL from Firebase Console → Functions → Dashboard → webhookHandler or stripeWebhook URL. For Stripe: go to Stripe Dashboard → Developers → Webhooks → Add endpoint. Paste the Cloud Function URL. Under 'Listen to' select 'Events on your account'. Choose the specific events you need: checkout.session.completed, payment_intent.succeeded, customer.subscription.updated, invoice.payment_failed. Click Add endpoint. Stripe immediately shows you the Signing secret — click Reveal and copy it, then store it: firebase functions:config:set stripe.webhook_secret='whsec_...' and redeploy. For Shopify: Shopify Admin → Settings → Notifications → Webhooks → Create webhook → choose event (e.g., orders/create) → paste Cloud Function URL → choose JSON format. For SendGrid: Settings → Mail Settings → Event Webhook → HTTP Post URL → paste URL → enable the events you want (delivered, opened, clicked, bounced). After registering, use each service's 'Send test webhook' button to verify your Cloud Function processes it correctly.
Expected result: Webhook endpoint is registered in Stripe, Shopify, or your chosen service. A test webhook triggers a new document in your Firestore webhook_events or payments collection.
Display webhook-driven data in FlutterFlow with a real-time Backend Query
Display webhook-driven data in FlutterFlow with a real-time Backend Query
Now that webhooks write to Firestore, configure FlutterFlow to display this data and react to changes in real-time. Create a PaymentStatus page (or add to your order confirmation page). Add a Backend Query on the page bound to the Firestore collection your webhook writes to — for payments, query the payments collection WHERE userId equals the current logged-in user's ID, ordered by createdAt descending. In the Backend Query settings, enable 'Single Time Query' OFF (leave it off to use real-time updates) — this makes FlutterFlow use a Firestore real-time listener that updates automatically when new documents arrive. Add a ListView or conditional Column that displays different content based on the payment status field. For example: if status == 'succeeded' show a green success card with the amount, if status == 'pending' show a loading spinner, if status == 'failed' show a red error message. This creates the effect of the app updating the moment Stripe confirms the payment — the webhook fires, Cloud Function writes to Firestore, Firestore listener in FlutterFlow triggers a UI update, all within 1-3 seconds.
Expected result: The FlutterFlow payment status page updates automatically within 1-3 seconds of a webhook arriving, showing the correct payment status without any page refresh.
Complete working example
1Architecture Flow2==================3External Service (Stripe/Shopify/SendGrid)4 │5 │ HTTP POST with signed payload6 ▼7Cloud Function: webhookHandler (HTTPS onRequest)8 │ 1. Verify HMAC-SHA256 signature9 │ 2. Check idempotency (event already processed?)10 │ 3. Return HTTP 200 immediately11 │ 4. Write to Firestore12 ▼13Firestore Collections14 │15 ├── webhook_events/{eventId}16 │ eventType, payload, receivedAt, processed17 │18 ├── payments/{paymentIntentId}19 │ userId, status, amount, currency, createdAt20 │21 ├── orders/{sessionId}22 │ status, customerEmail, amountTotal, updatedAt23 │24 └── email_events/{messageId}25 event (delivered/opened/clicked), email, timestamp26 │27 │ Real-time Firestore listener28 ▼29FlutterFlow App30 └── Backend Query (real-time, no 'Single Time Query')31 └── UI updates automatically on new Firestore doc3233Cloud Function URLs to Register34================================35https://us-central1-{project}.cloudfunctions.net/stripeWebhook36 → Register in: Stripe Dashboard → Developers → Webhooks37 → Events: checkout.session.completed, payment_intent.succeeded38 → Signature: HMAC-SHA256 via stripe.webhooks.constructEvent()3940https://us-central1-{project}.cloudfunctions.net/shopifyWebhook41 → Register in: Shopify Admin → Settings → Notifications → Webhooks42 → Events: orders/create, orders/fulfilled43 → Signature: X-Shopify-Hmac-Sha256 header4445https://us-central1-{project}.cloudfunctions.net/sendgridWebhook46 → Register in: SendGrid → Mail Settings → Event Webhook47 → Events: delivered, opened, clicked, bounced48 → Signature: X-Twilio-Email-Event-Webhook-Signature4950FlutterFlow Backend Query (payments page)51==========================================52Collection: payments53Filter: userId == currentUserRef54Order: createdAt Descending55Limit: 1056Real-time: ON (stream updates)5758Conditional UI:59 if status == 'succeeded' → show green success card60 if status == 'pending' → show CircularProgressIndicator61 if status == 'failed' → show red error with retry buttonCommon mistakes
Why it's a problem: Registering your FlutterFlow app's published URL as the webhook endpoint in Stripe or Shopify
How to avoid: Register a Firebase Cloud Function HTTPS URL as the webhook endpoint. Cloud Functions are real servers that run your code in response to HTTP POST requests. The URL format is: https://us-central1-{firebase-project-id}.cloudfunctions.net/{functionName}.
Why it's a problem: Not verifying the webhook signature and trusting all incoming POST requests to the Cloud Function
How to avoid: Always verify the HMAC signature before processing any webhook. For Stripe: use stripe.webhooks.constructEvent(req.rawBody, sig, secret) which throws an error if invalid. For Shopify: compare the X-Shopify-Hmac-Sha256 header against HMAC-SHA256 of the raw body using your webhook secret. Reject all requests that fail signature verification with HTTP 401.
Why it's a problem: Returning HTTP 500 or throwing an error when webhook processing fails, causing the external service to retry the same webhook hundreds of times
How to avoid: Always return HTTP 200 to acknowledge receipt. Handle processing errors by writing to a dead_letter_queue Firestore collection for manual investigation. Use idempotency checks (if Firestore document already exists, return 200 without reprocessing) to handle duplicate deliveries safely.
Best practices
- Always verify webhook signatures before processing — use the HMAC secret provided by each service, not just checking the HTTP origin
- Return HTTP 200 immediately after signature verification, then process the event asynchronously to stay within external service timeout windows (Stripe: 20 seconds)
- Implement idempotency using the webhook event ID as the Firestore document ID — check if it already exists before processing to handle duplicate deliveries gracefully
- Use a webhook_events Firestore collection as a raw log of all received webhooks — this gives you a full audit trail and lets you replay events if your processing logic had a bug
- Set up Firestore TTL policies to automatically delete old webhook_events documents after 90 days to avoid unbounded collection growth
- Test webhooks locally using the Stripe CLI (stripe listen) or ngrok before deploying — this shortens your development feedback loop significantly
- Monitor Cloud Function error rates in Firebase Console → Functions → Dashboard — webhook processing failures appear as function execution errors
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I need to receive Stripe webhooks in my FlutterFlow app. Write a Firebase Cloud Function in Node.js that: (1) receives POST requests from Stripe at an HTTPS endpoint, (2) verifies the Stripe-Signature header using stripe.webhooks.constructEvent with my webhook signing secret, (3) returns HTTP 200 immediately, (4) processes the checkout.session.completed and payment_intent.succeeded events by writing payment status to Firestore, including the userId from the payment metadata. Include idempotency checking using the event ID.
Add a real-time Backend Query to the OrderConfirmation page that reads from the payments Firestore collection filtered by the current user's document reference, ordered by createdAt descending. Display a conditional Container: show a green 'Payment confirmed' card when the status field is 'succeeded', and a loading spinner when it is 'pending'.
Frequently asked questions
Can I use Zapier instead of Cloud Functions to receive webhooks?
Yes — Zapier Catch Hook gives you a webhook URL that receives POST requests from external services without writing any code. When Zapier receives the webhook, it can trigger a Zap that writes data to a Google Sheet, sends a Slack message, or calls another API. To get the data into Firestore for FlutterFlow: add a Zapier step that makes an HTTP POST to your FlutterFlow API Call, or use a Zapier Firebase action if available. Zapier is simpler than Cloud Functions for basic routing but has limitations on custom signature verification and complex payload processing.
How do I test my webhook Cloud Function before going live?
Three approaches: (1) Stripe CLI: install stripe CLI, run stripe login, then stripe listen --forward-to localhost:5001/{project}/us-central1/stripeWebhook to forward real Stripe test events to your locally running Cloud Function emulator. (2) ngrok: run your Cloud Function emulator locally (firebase emulators:start --only functions) and use ngrok to expose localhost:5001 with a public URL — register that ngrok URL as the webhook endpoint in Stripe's test mode. (3) Test buttons: most services (Stripe, Shopify, SendGrid) have a 'Send test webhook' button in their dashboard that sends a sample payload to your registered URL.
What happens if my Cloud Function is down when a webhook fires?
External services retry failed webhooks. Stripe retries with exponential backoff over 3 days (about 15 attempts: 5 min, 10 min, 20 min, 40 min, etc.). Shopify retries 19 times over 48 hours. SendGrid retries for 72 hours. Cloud Functions are serverless — they have no planned downtime and auto-scale to handle load. If a Cloud Function execution fails (not returns non-200), Firebase marks it as an error and the external service retries. In practice, Cloud Functions have 99.95%+ availability.
How do I handle webhooks for multiple users in my FlutterFlow app?
Include the userId in the webhook metadata when initiating the action. For Stripe: when creating a PaymentIntent, add metadata: {userId: currentUserId}. The webhook payload includes this metadata, so your Cloud Function writes the payment document with userId: event.data.object.metadata.userId. The FlutterFlow Backend Query filters payments by the current user's ID, so each user only sees their own payment confirmations. For webhooks that do not support metadata (some Shopify events), match the customer email from the webhook to the userId in your Firestore users collection.
How much does it cost to run webhook Cloud Functions?
Firebase Cloud Functions pricing: first 2 million invocations/month are free, then $0.40 per million. Each webhook = one invocation. For a typical SaaS app with 1,000 paying customers making one purchase per month: 1,000 invocations/month, well within the free tier. Even at 100,000 monthly transactions, you pay $0 for invocations and around $2-5 for compute time. Cloud Functions for webhook handling is effectively free for most FlutterFlow apps.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation