Skip to main content
RapidDev - Software Development Agency
flutterflow-tutorials

How to Set Up Webhooks for External Services in FlutterFlow

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.

What you'll learn

  • Why FlutterFlow apps cannot receive webhooks directly and why a Cloud Function is required
  • How to deploy a Cloud Function HTTPS endpoint that receives and verifies webhook payloads
  • How to write webhook data to Firestore so FlutterFlow's real-time Backend Query picks up the update
  • How to handle webhook signature verification for Stripe and other services to prevent spoofed requests
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner13 min read25-35 minFlutterFlow Free+ (Firebase Cloud Functions required — Blaze plan)March 2026RapidDev Engineering Team
TL;DR

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

1

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.

2

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.

functions/index.js
1// functions/index.js — Generic webhook receiver
2const functions = require('firebase-functions');
3const admin = require('firebase-admin');
4
5admin.initializeApp();
6
7exports.webhookHandler = functions.https.onRequest(async (req, res) => {
8 // Always respond quickly — return 200 before heavy processing
9 // External services retry if they don't get 200 within their timeout
10
11 if (req.method !== 'POST') {
12 res.status(405).send('Method Not Allowed');
13 return;
14 }
15
16 const db = admin.firestore();
17
18 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; }
22
23 // Step 2: Parse the event
24 const event = req.body;
25 const eventType = event.type || event.event || 'unknown';
26 const eventId = event.id || db.collection('webhook_events').doc().id;
27
28 // 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 }
34
35 // Step 4: Write to Firestore
36 await db.collection('webhook_events').doc(eventId).set({
37 eventType,
38 payload: event,
39 receivedAt: admin.firestore.FieldValue.serverTimestamp(),
40 processed: false,
41 });
42
43 // Step 5: Return 200 immediately
44 res.status(200).json({ received: true });
45
46 // Step 6: Process the event AFTER returning (fire-and-forget)
47 await processWebhookEvent(db, eventType, event);
48
49 } catch (err) {
50 console.error('Webhook error:', err);
51 // Return 200 even on processing errors to prevent retries
52 // Log to a dead_letter_queue for manual inspection
53 res.status(200).json({ received: true, processingError: err.message });
54 }
55});
56
57async 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 needed
77}

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.

3

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.

functions/stripe_webhook.js
1// Stripe webhook signature verification
2// Install: cd functions && npm install stripe
3const stripe = require('stripe')(functions.config().stripe.secret_key);
4
5exports.stripeWebhook = functions.https.onRequest(
6 // CRITICAL: Use raw body for Stripe signature verification
7 // In firebase.json, add rewrite or use express with raw body parser
8 async (req, res) => {
9 const sig = req.headers['stripe-signature'];
10 const webhookSecret = functions.config().stripe.webhook_secret;
11
12 let event;
13 try {
14 // req.rawBody is the raw Buffer — required for HMAC verification
15 // Firebase Functions automatically provides req.rawBody
16 event = stripe.webhooks.constructEvent(
17 req.rawBody,
18 sig,
19 webhookSecret
20 );
21 } catch (err) {
22 console.error('Stripe signature verification failed:', err.message);
23 res.status(400).send(`Webhook Error: ${err.message}`);
24 return;
25 }
26
27 // Signature verified — safe to process
28 const db = admin.firestore();
29 res.status(200).json({ received: true });
30
31 // Process after returning 200
32 if (event.type === 'payment_intent.succeeded') {
33 const paymentIntent = event.data.object;
34 const userId = paymentIntent.metadata?.userId;
35
36 await db.collection('payments').doc(paymentIntent.id).set({
37 userId,
38 status: 'succeeded',
39 amount: paymentIntent.amount / 100, // cents to dollars
40 currency: paymentIntent.currency.toUpperCase(),
41 createdAt: admin.firestore.FieldValue.serverTimestamp(),
42 });
43
44 if (userId) {
45 // Update user subscription status if applicable
46 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.

4

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.

5

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

Webhook Integration Architecture
1Architecture Flow
2==================
3External Service (Stripe/Shopify/SendGrid)
4
5 HTTP POST with signed payload
6
7Cloud Function: webhookHandler (HTTPS onRequest)
8 1. Verify HMAC-SHA256 signature
9 2. Check idempotency (event already processed?)
10 3. Return HTTP 200 immediately
11 4. Write to Firestore
12
13Firestore Collections
14
15 webhook_events/{eventId}
16 eventType, payload, receivedAt, processed
17
18 payments/{paymentIntentId}
19 userId, status, amount, currency, createdAt
20
21 orders/{sessionId}
22 status, customerEmail, amountTotal, updatedAt
23
24 email_events/{messageId}
25 event (delivered/opened/clicked), email, timestamp
26
27 Real-time Firestore listener
28
29FlutterFlow App
30 Backend Query (real-time, no 'Single Time Query')
31 UI updates automatically on new Firestore doc
32
33Cloud Function URLs to Register
34================================
35https://us-central1-{project}.cloudfunctions.net/stripeWebhook
36 Register in: Stripe Dashboard Developers Webhooks
37 Events: checkout.session.completed, payment_intent.succeeded
38 Signature: HMAC-SHA256 via stripe.webhooks.constructEvent()
39
40https://us-central1-{project}.cloudfunctions.net/shopifyWebhook
41 Register in: Shopify Admin Settings Notifications Webhooks
42 Events: orders/create, orders/fulfilled
43 Signature: X-Shopify-Hmac-Sha256 header
44
45https://us-central1-{project}.cloudfunctions.net/sendgridWebhook
46 Register in: SendGrid Mail Settings Event Webhook
47 Events: delivered, opened, clicked, bounced
48 Signature: X-Twilio-Email-Event-Webhook-Signature
49
50FlutterFlow Backend Query (payments page)
51==========================================
52Collection: payments
53Filter: userId == currentUserRef
54Order: createdAt Descending
55Limit: 10
56Real-time: ON (stream updates)
57
58Conditional UI:
59 if status == 'succeeded' show green success card
60 if status == 'pending' show CircularProgressIndicator
61 if status == 'failed' show red error with retry button

Common 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.

ChatGPT Prompt

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.

FlutterFlow Prompt

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.

RapidDev

Talk to an Expert

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

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

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.