# How to Set Up Webhooks for External Services in FlutterFlow

- Tool: FlutterFlow
- Difficulty: Beginner
- Time required: 25-35 min
- Compatibility: FlutterFlow Free+ (Firebase Cloud Functions required — Blaze plan)
- Last updated: March 2026

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

## Before you start

- 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 — Generic webhook receiver
const functions = require('firebase-functions');
const admin = require('firebase-admin');

admin.initializeApp();

exports.webhookHandler = functions.https.onRequest(async (req, res) => {
  // Always respond quickly — return 200 before heavy processing
  // External services retry if they don't get 200 within their timeout

  if (req.method !== 'POST') {
    res.status(405).send('Method Not Allowed');
    return;
  }

  const db = admin.firestore();

  try {
    // Step 1: Verify signature (see next step for Stripe example)
    // const isValid = verifySignature(req);
    // if (!isValid) { res.status(401).send('Invalid signature'); return; }

    // Step 2: Parse the event
    const event = req.body;
    const eventType = event.type || event.event || 'unknown';
    const eventId = event.id || db.collection('webhook_events').doc().id;

    // Step 3: Prevent duplicate processing (idempotency)
    const existingDoc = await db.collection('webhook_events').doc(eventId).get();
    if (existingDoc.exists) {
      res.status(200).send('Already processed');
      return;
    }

    // Step 4: Write to Firestore
    await db.collection('webhook_events').doc(eventId).set({
      eventType,
      payload: event,
      receivedAt: admin.firestore.FieldValue.serverTimestamp(),
      processed: false,
    });

    // Step 5: Return 200 immediately
    res.status(200).json({ received: true });

    // Step 6: Process the event AFTER returning (fire-and-forget)
    await processWebhookEvent(db, eventType, event);

  } catch (err) {
    console.error('Webhook error:', err);
    // Return 200 even on processing errors to prevent retries
    // Log to a dead_letter_queue for manual inspection
    res.status(200).json({ received: true, processingError: err.message });
  }
});

async function processWebhookEvent(db, eventType, event) {
  if (eventType === 'payment_intent.succeeded') {
    const pi = event.data?.object;
    await db.collection('payments').doc(pi.id).set({
      status: 'succeeded',
      amount: pi.amount,
      currency: pi.currency,
      customerId: pi.customer,
      updatedAt: admin.firestore.FieldValue.serverTimestamp(),
    });
  } else if (eventType === 'checkout.session.completed') {
    const session = event.data?.object;
    await db.collection('orders').doc(session.id).set({
      status: 'paid',
      customerEmail: session.customer_email,
      amountTotal: session.amount_total,
      updatedAt: admin.firestore.FieldValue.serverTimestamp(),
    });
  }
  // Add more event types as needed
}
```

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

```
// Stripe webhook signature verification
// Install: cd functions && npm install stripe
const stripe = require('stripe')(functions.config().stripe.secret_key);

exports.stripeWebhook = functions.https.onRequest(
  // CRITICAL: Use raw body for Stripe signature verification
  // In firebase.json, add rewrite or use express with raw body parser
  async (req, res) => {
    const sig = req.headers['stripe-signature'];
    const webhookSecret = functions.config().stripe.webhook_secret;

    let event;
    try {
      // req.rawBody is the raw Buffer — required for HMAC verification
      // Firebase Functions automatically provides req.rawBody
      event = stripe.webhooks.constructEvent(
        req.rawBody,
        sig,
        webhookSecret
      );
    } catch (err) {
      console.error('Stripe signature verification failed:', err.message);
      res.status(400).send(`Webhook Error: ${err.message}`);
      return;
    }

    // Signature verified — safe to process
    const db = admin.firestore();
    res.status(200).json({ received: true });

    // Process after returning 200
    if (event.type === 'payment_intent.succeeded') {
      const paymentIntent = event.data.object;
      const userId = paymentIntent.metadata?.userId;

      await db.collection('payments').doc(paymentIntent.id).set({
        userId,
        status: 'succeeded',
        amount: paymentIntent.amount / 100, // cents to dollars
        currency: paymentIntent.currency.toUpperCase(),
        createdAt: admin.firestore.FieldValue.serverTimestamp(),
      });

      if (userId) {
        // Update user subscription status if applicable
        await db.collection('users').doc(userId).update({
          subscriptionStatus: 'active',
          lastPaymentAt: admin.firestore.FieldValue.serverTimestamp(),
        });
      }
    }
  }
);
```

**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 code example

File: `Webhook Integration Architecture`

```text
Architecture Flow
==================
External Service (Stripe/Shopify/SendGrid)
  │
  │  HTTP POST with signed payload
  ▼
Cloud Function: webhookHandler (HTTPS onRequest)
  │  1. Verify HMAC-SHA256 signature
  │  2. Check idempotency (event already processed?)
  │  3. Return HTTP 200 immediately
  │  4. Write to Firestore
  ▼
Firestore Collections
  │
  ├── webhook_events/{eventId}
  │     eventType, payload, receivedAt, processed
  │
  ├── payments/{paymentIntentId}
  │     userId, status, amount, currency, createdAt
  │
  ├── orders/{sessionId}
  │     status, customerEmail, amountTotal, updatedAt
  │
  └── email_events/{messageId}
        event (delivered/opened/clicked), email, timestamp
  │
  │  Real-time Firestore listener
  ▼
FlutterFlow App
  └── Backend Query (real-time, no 'Single Time Query')
      └── UI updates automatically on new Firestore doc

Cloud Function URLs to Register
================================
https://us-central1-{project}.cloudfunctions.net/stripeWebhook
  → Register in: Stripe Dashboard → Developers → Webhooks
  → Events: checkout.session.completed, payment_intent.succeeded
  → Signature: HMAC-SHA256 via stripe.webhooks.constructEvent()

https://us-central1-{project}.cloudfunctions.net/shopifyWebhook
  → Register in: Shopify Admin → Settings → Notifications → Webhooks
  → Events: orders/create, orders/fulfilled
  → Signature: X-Shopify-Hmac-Sha256 header

https://us-central1-{project}.cloudfunctions.net/sendgridWebhook
  → Register in: SendGrid → Mail Settings → Event Webhook
  → Events: delivered, opened, clicked, bounced
  → Signature: X-Twilio-Email-Event-Webhook-Signature

FlutterFlow Backend Query (payments page)
==========================================
Collection: payments
Filter: userId == currentUserRef
Order: createdAt Descending
Limit: 10
Real-time: ON (stream updates)

Conditional UI:
  if status == 'succeeded' → show green success card
  if status == 'pending'   → show CircularProgressIndicator
  if status == 'failed'    → show red error with retry button
```

## Common mistakes

- **Registering your FlutterFlow app's published URL as the webhook endpoint in Stripe or Shopify** — FlutterFlow's published URL serves your web app to users — it is not an HTTPS server that can process POST requests. Stripe or Shopify will receive a 200 OK from the HTML page served at that URL, not from webhook processing code. The webhook payload is silently discarded. Your app will never know the payment succeeded. Fix: 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}.
- **Not verifying the webhook signature and trusting all incoming POST requests to the Cloud Function** — Anyone who discovers your Cloud Function URL can send fake webhook events — simulating a payment_intent.succeeded to mark unpaid orders as paid, or sending fake Shopify orders to flood your database. Without signature verification, you cannot distinguish legitimate Stripe webhooks from forged requests. Fix: 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.
- **Returning HTTP 500 or throwing an error when webhook processing fails, causing the external service to retry the same webhook hundreds of times** — Services like Stripe retry failed webhooks with exponential backoff for up to 3 days. If your Cloud Function throws an error on every attempt (e.g., a bug in processing logic), Stripe sends the same webhook every few hours until it expires. You receive hundreds of duplicate events and burn Cloud Function invocations. Fix: 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

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

---

Source: https://www.rapidevelopers.com/flutterflow-tutorials/how-to-set-up-webhooks-for-external-services-in-flutterflow
© RapidDev — https://www.rapidevelopers.com/flutterflow-tutorials/how-to-set-up-webhooks-for-external-services-in-flutterflow
