What are Webhooks and How to Use Them in Your Flutter Backend

Webhooks are an important building block when writing backend integrations for your Flutter apps.

Common use cases for webhooks include:

  • Implementing in-app subscriptions with RevenueCat or Glassfy
  • Accepting payments using Stripe or PayPal

But if you’ve never implemented webhooks before, you may find them a bit confusing.

And if you’ve been looking for a “guide to webhooks for Flutter developers”, you’re in the right place. 🙂

Inside, we’ll answer these questions:

  • What are webhooks
  • When to use them
  • How to create and deploy webhooks on your backend
  • How to register webhooks using Stripe
  • How to secure your webhooks with signature verification

We’ll start with a conceptual overview and explain where webhooks fit in the big picture.

Then, I’ll share a real-world example showing how to use webhooks when integrating Stripe payments with a Firebase backend.

This is meant to be an introduction to webhooks and not a complete step-by-step tutorial. At the end, I’ll share some additional resources that you can follow if you want to add Stripe to your backend.

Ready? Let’s go!

What is a Webhook?

Webhooks are a technique used for server-to-server communication.

Using webhooks, a 3rd party server (such as Stripe) can notify your backend when certain events take place (e.g. a customer purchases a product).

Webhooks are much like realtime listeners, with the crucial difference that events are delivered to your backend rather than your Flutter client, as illustrated in this diagram:

A payment workflow showing how Stripe calls a webhook that is deployed in the Firebase backend
A payment workflow showing how Stripe calls a webhook that is deployed in the Firebase backend

Note how webhooks are not request-based (like regular API calls). Rather, they are event-based, meaning that they run when a specific event takes place. For this reason, they are also known as web callbacks or HTTP push APIs.

Since servers communicate via HTTP, a webhook is a public HTTP endpoint that can be called by making a POST request.

For example, if you deploy a webhook as a Cloud Function using Firebase, the URL may look like this:

https://us-central1-flutter-firebase-masterclass.cloudfunctions.net/stripeWebhook

But when should we use webhooks in the first place?

When to Use Webhooks?

Imagine we’re writing an eCommerce app where customers can purchase products using Stripe:

A checkout flow using a native payment sheet on mobile
A checkout flow using a native payment sheet on mobile

In this scenario:

  • the Flutter app reads the products from your backend (e.g. Firestore) and shows them in the UI (screen 1).
  • with the Flutter Stripe SDK, we can show a native payment sheet and take payments from the user (screen 2).
  • if the payment is successful, we show the order confirmation to the user (screen 3).

Note that it would be insecure to receive and process the Stripe payment events on the client. And it would be unreliable, too, since the Flutter app could be backgrounded or killed at any time by the OS.

Instead, we should write and deploy our webhook on the backend, since this is a trusted and secure environment that we have control over.

And once the webhook is called by Stripe, it can process the incoming event and fulfil the order:

A payment workflow showing how Stripe calls a webhook that is deployed in the Firebase backend
A payment workflow showing how Stripe calls a webhook that is deployed in the Firebase backend

In summary, the checkout process works as follows:

  1. The user pays with the native payment sheet
  2. Stripe processes the payment and calls a webhook to notify our backend when the payment has succeeded
  3. Our backend fulfils the order by updating some data in the DB (Firestore)
  4. The Flutter listens to realtime updates from the DB and shows the order confirmation

This example was Stripe-specific, but the same workflow would apply if you’re purchasing subscriptions using RevenueCat:

A payment workflow showing how RevenueCat calls a webhook that is deployed in the Firebase backend
A payment workflow showing how RevenueCat calls a webhook that is deployed in the Firebase backend

In this case, the business logic and the APIs may be different, but the steps are the same.


Now that we understand how webhooks work in theory, let’s see how to use them in practice.

The following example is based on Stripe and Firebase Cloud Functions, but the same concepts apply if you’re using a different integration (e.g. RevenueCat) or have a different backend (Node.js, PHP, or Python).

Stripe Integration Example with Webhooks

The Stripe documentation about webhooks says that we need to register a webhook endpoint in the Developer dashboard so Stripe knows where to deliver events.

We can also learn that the webhook URL format looks like this:

https://<your-website>/<your-webhook-endpoint>

So let’s go ahead and create the webhook.

Creating the Webhook as a Firebase Cloud Function

If we’re using Firebase on the backend, we can do this by writing and deploying an HTTP callable function, which looks like this:

// index.ts import * as functions from "firebase-functions/v2" import * as logger from "firebase-functions/logger" // ! This is insecure, don't use it in production exports.stripeWebhook = functions.https.onRequest( (req, res) => { const event = req.body const eventType = event.type if (eventType === 'payment_intent.succeeded') { // TODO: Extract the data from the event and fulfil order logger.log(event) } })

All this function does is check if the event type is payment_intent.succeeded and prints it to the console.

As it stands, the function above is insecure because we haven’t added signature verification yet. We’ll tackle this shortly.

If we run firebase deploy --only functions, it will be deployed to a URL that looks like this:

https://<region>-<project-id>.cloudfunctions.net/stripeWebhook

Registering the Webhook on the Stripe Dashboard

Once we know the webhook URL, we can head to the Stripe dashboard and select the option to add a new endpoint:

A webhook endpoint can be added in the Stripe dashboard
A webhook endpoint can be added in the Stripe dashboard

Then, we can paste the URL in the Endpoint URL field:

The webhook configuration page in the Stripe dashboard
The webhook configuration page in the Stripe dashboard

We should also click on “Select events” and tell Stripe which events should be sent to the webhook (in this case: payment_intent.succeeded).

Once this is done, we’ll be taken back to the webhooks page, where we can inspect our webhook:

Once a webhook has been added, it will appear in the webhooks page
Once a webhook has been added, it will appear in the webhooks page

If we run our app at this stage and make a payment, we should see a log in the Google Cloud Logs Explorer, confirming that our Cloud Function is processing the incoming events from Stripe.

But so far, we have ignored something very important. 👇

Signature Verification with the Webhook Signing Secret

Let’s take another look at our example webhook:

import * as functions from "firebase-functions/v2" import * as logger from "firebase-functions/logger" exports.stripeWebhook = functions.https.onRequest( (req, res) => { // ! This is insecure, don't use it in production const event = req.body const eventType = event.type if (eventType === 'payment_intent.succeeded') { // TODO: Extract the data from the event and fulfil order logger.log(event) } })

The function above has one big security flaw, as it does nothing to check if the webhook request comes from Stripe (and not a server acting like Stripe).

And if we want to verify the authenticity of the request, we have to implement signature verification by following these steps:

  • Get the Stripe secret key from the API keys page and the webhook signing secret from the webhook we just created
  • Store the keys securely with the Firebase CLI
  • Access the keys in the stripeWebook function and use them to verify the signature

Here’s an updated version of the function above, showing how this works in practice:

import * as functionsV2 from "firebase-functions/v2" import {defineSecret} from "firebase-functions/params" import Stripe from "stripe" import * as logger from "firebase-functions/logger" // * Define the Stripe Secret Key and Stripe Webhook Secret Key // * To set them, run: // * firebase functions:secrets:set STRIPE_SECRET_KEY // * firebase functions:secrets:set STRIPE_WEBHOOK_SECRET_KEY // * For more info, read: // * https://codewithandrea.com/articles/api-keys-2ndgen-cloud-functions-firebase/ const stripeSecretKey = defineSecret("STRIPE_SECRET_KEY") const stripeWebhookSecretKey = defineSecret("STRIPE_WEBHOOK_SECRET_KEY") exports.stripeWebhook = functionsV2.https.onRequest( { secrets: [stripeSecretKey, stripeWebhookSecretKey] }, (req, res) => { // get the first secret key value const secretKey = stripeSecretKey.value() // if it's empty, throw an error if (secretKey.length === 0) { logger.error("⚠️ Stripe Secret Key is not set") res.sendStatus(400) } // get the second secret key value const webhookSecretKey = stripeWebhookSecretKey.value() // if it's empty, throw an error if (webhookSecretKey.length === 0) { logger.error("⚠️ Stripe webhook secret is not set") res.sendStatus(400) } const stripe = new Stripe(secretKey, { apiVersion: "2023-08-16", typescript: true, }) let event: Stripe.Event; try { // Retrieve the event by verifying the signature using the raw body and secret. event = stripe.webhooks.constructEvent( // Note: to perform the verfication correctlly, we must use the rawBody // See: https://medium.com/@GaryHarrower/working-with-stripe-webhooks-firebase-cloud-functions-5366c206c6c req.rawBody, req.headers['stripe-signature'] || [], webhookSecretKey ) } catch (err) { logger.error(`⚠️ Webhook signature verification failed.`) res.sendStatus(400) return } // Process the event const eventType = event.type if (eventType === 'payment_intent.succeeded') { // TODO: Extract the data from the event and fulfil order logger.log(event) } })

If you’re not familiar with how to work with API keys in Firebase, read: How to Secure API Keys with 2nd-Gen Cloud Functions and Firebase

Once we deploy the updated Cloud Function, all webhook requests that don’t come from Stripe will be rejected with a 400 error.

However, we still have one step to complete. 👇

Order fulfilment

Order fulfilment is all about one thing: deliver to the user what he/she paid for:

If we’re working on an eCommerce app, this entails writing some data to Firestore so we can show an order confirmation to the user:

We can show a confirmation page after an order has been completed
We can show a confirmation page after an order has been completed

Or if we’re creating a game and the user just purchased some additional levels via IAP, we can unlock them.

The order fulfilment logic will depend on what kind of app we’re writing, so I won’t share any example code here. But the most important thing is that:

  • we can fulfil the order on the backend by writing some data to the DB
  • the Flutter app can read that data and update the UI
A payment workflow showing how Stripe calls a webhook that is deployed in the Firebase backend
A payment workflow showing how Stripe calls a webhook that is deployed in the Firebase backend

Note that by using webhooks on the backend in addition to realtime updates on the client, the entire system architecture becomes event-based. This is very powerful since our Flutter app can now react to events that occur outside our backend. 🚀

Stripe Integration: Is There an Easier Way?

As we have seen, working with Stripe webhooks is a bit tricky because we have to:

  • Store the API keys securely
  • Load them inside the webhook
  • Secure the webhook using signature verification

The good news is that if we use the Stripe payments extension for Firebase, much of the hard work is done for us. And rather than writing a lot of the backend code by hand, we can just configure the extension and let it handle the incoming webhook events for us:

stripe-extension-apis.png
stripe-extension-apis.png

I won’t cover all the details here. But if you want to learn more, my Flutter & Firebase course contains an entire module about how to build a seamless checkout experience with Stripe.

Conclusion

Webhooks are an important building block when building backend integrations.

They make it possible for a server to notify another server (such as your backend) when certain events take place. And I hope that after reading this article, you have a better idea of how they work.

Additional Resources

If you ever need to add Stripe to your Flutter app, make sure to use the flutter_stripe package on pub.dev.

And if you use Firebase on the backend, using the Stripe payments extension will make your life much easier.

But if you use a different backend, then you’ll have to write, test, and deploy your own webhook, and the official Stripe docs can help with that:

Finally, if you want to learn how to build an eCommerce app, complete with checkout flows on mobile and web, my Flutter & Firebase course is a very comprehensive resource.

New Flutter & Firebase Course

If you want to ship your Flutter apps faster, Firebase is a great choice. And in this new course, I cover all the most important features, including Firebase Auth, Cloud Firestore, Firebase Storage, Cloud Functions, and Firebase Extensions. 👇

Want More?

Invest in yourself with my high-quality Flutter courses.

Flutter Foundations Course

Flutter Foundations Course

Learn about State Management, App Architecture, Navigation, Testing, and much more by building a Flutter eCommerce app on iOS, Android, and web.

The Complete Dart Developer Guide

The Complete Dart Developer Guide

Learn Dart Programming in depth. Includes: basic to advanced topics, exercises, and projects. Fully updated to Dart 2.15.

Flutter Animations Masterclass

Flutter Animations Masterclass

Master Flutter animations and build a completely custom habit tracking application.