tutorial // Oct 15, 2021
How to Securely Handle Stripe Webhooks
How to receive and parse Stripe webhooks, validate their contents, and use their data in your application.
![How to Securely Handle Stripe Webhooks](/default_tutorial_seo_graphic.webp)
Getting Started
For this tutorial, we're going to use the CheatCode Node.js Boilerplate as a starting point for our work. To start, let's clone a copy from Github:
Terminal
git clone https://github.com/cheatcode/nodejs-server-boilerplate
Next, cd
into the project and install its dependencies:
Terminal
cd nodejs-server-boilerplate && npm install
Next, we need to add one more dependency stripe
which will help us to parse and authenticate the webhooks we receive from Stripe:
Terminal
npm i stripe
Finally, go ahead and start up the development server:
Terminal
npm run dev
With that, we're ready to get started.
Obtaining a secret key and webhook signing secret
Before we dig into the code, the first thing we need to do is get access to two things: our Stripe Secret Key and our Webhook Signing Secret.
To obtain these, you will need to have an existing Stripe account. If you don't have one already, you can sign up here. After you have access to the Stripe dashboard, you can continue with the steps below.
Once you're logged in, to locate your Secret Key:
- First, in the top-right corner, make sure that you've toggled the "Test mode" toggle so that it's lit up (as of writing this will turn orange when activated).
- To the left of that toggle, click the "Developers" button.
- On the next page, in the left-hand navigation menu, select the "API keys" tab.
- Under the "Standard keys" block on this page, locate your "Secret key" and click the "Reveal test key" button.
- Copy this key (keep it safe as this is used to perform transactions with your Stripe account).
Next, once we have our secret key, we need to open up the project we just cloned and navigate to the /settings-development.json
file:
/settings-development.json
const settings = {
"authentication": { ... },
"databases": { ... },
"smtp": { ... },
"stripe": {
"secretKey": "<Paste your secret key here>"
},
"support": { ... },
"urls": { ... }
};
export default settings;
In this file, alphabetically near the bottom of the exported settings
object, we want to add a new property stripe
and set it to an object with a single property: secretKey
. For the value of this property, we want to paste in the secret key you copied from the Stripe dashboard above. Paste it in and then save this file.
In the boilerplate we cloned above, there's a built-in mechanism for loading our settings based on the value of
process.env.NODE_ENV
(e.g., development, staging, or production). Here, we assume we're developing on our local machine and add our keys to the settings-development.json file. If we were deploying to production, we would make this change in our settings-production.json file, using our live secret key as opposed to our test secret key like we see above.
Next, we need to obtain one more value: our webhook signing secret. To do this, we need to create a new endpoint. From the same "Developers" tab in the Stripe dashboard, from the left-hand navigation (where you clicked on "API keys"), locate the "Webhooks" option.
On this page, you will either see a prompt to create your first webhook endpoint, or, the option to add another endpoint. Click the option to "Add endpoint" to reveal the webhook configuration screen.
In the window that reveals itself, we want to customize the "Endpoint URL" field and then select the events we want to listen to from Stripe.
In the URL field, we want to use the domain name where our app is running. For example, if we were in production, we might do something like https://cheatcode.co/webhooks/stripe
. For our example, because we anticipate our app running on localhost, we need a URL that points back to our machine.
For this, the tool Ngrok is highly recommended. It's a free service (with paid options for additional features) that allows you to create a tunnel back to your computer via the internet. For our demo, the https://tunnel.cheatcode.co/webhooks/stripe
endpoint we're using is pointing back to our localhost via Ngrok (free plans get a domain at <randomId>.ngrok.io
, but paid plans can use a custom domain like the tunnel.cheatcode.co
one we're using here).
The important part here is the part after the domain: /webhooks/stripe
. This is the route that's defined within our application where we expect webhooks to be sent.
Next, just below this, we want to click the "Select events" button under the "Select events to listen to" header. In this next window, Stripe gives us the option to customize which events it will send to our endpoint. By default, they will send events of all types, but it's recommended that you customize this for the needs of your application.
For our demo, we're going to add two event types: invoice.payment_succeeded
(sent whenever we successfuly receive a payment from a customer) and invoice.payment_failed
(sent whenever a payment from a customer fails).
Once you've added these—or whatever events you prefer—click the "Add endpoint" button.
Finally, to get your Webhook Signing Secret, from the page shown after creating your endpoint, in the row beneath the URL, locate the "Signing secret" box and click the "Reveal" link inside of it. Copy the secret that's revealed.
/settings-development.json
...
"stripe": {
"secretKey": "",
"webhookSecret": "<Paste your secret here>"
},
...
}
Back in your /settings-development.json
file, under the stripe
object that we added earlier, add an additional property webhookSecret
and set the value to the secret you just copied from the Stripe dashboard.
Adding middleware to parse the webhook request
Now we're ready to get into the code. First, in order to ensure we properly receive webhooks from Stripe, we need to make sure we're handling the request body we'll receive from Stripe properly.
Inside of the project we cloned above, we'll want to navigate to the /middleware/bodyParser.js
file:
/middleware/bodyParser.js
import bodyParser from "body-parser";
export default (req, res, next) => {
const contentType = req.headers["content-type"];
if (req.headers["stripe-signature"]) {
return bodyParser.raw({ type: "*/*", limit: "50mb" })(req, res, next);
}
if (contentType && contentType === "application/x-www-form-urlencoded") {
return bodyParser.urlencoded({ extended: true })(req, res, next);
}
return bodyParser.json()(req, res, next);
};
In this file, we'll find the existing body parser middleware for the boilerplate. In here, you will find a series of conditional statements which change how the request body should be parsed depending on the origin of the request and its specified Content-Type
header (this is the mechanism used in an HTTP request to designate the format of the data in the body field on a request).
Generally speaking, request body's will typically be sent as either JSON data or as URL form encoded data. These two types are already handled in our middleware.
In order to handle requests from Stripe properly, we need to support a raw HTTP body (this is the unparsed HTTP request body, usually plain text or binary data). We need this for Stripe as this is what they expect from within their own webhook validator function (what we'll look at later).
In the code above, we add an additional if
statement to check for an HTTP header stripe-signature
on all inbound requests to our app. The function exported above is called to via the /middleware/index.js
file which is itself called before an inbound request is handed off to our routes in /index.js
for resolution.
If we see the HTTP header stripe-signature
, we know that we're receiving an inbound request from Stripe (a webhook) and that we want to ensure that the body for that request remains in its raw state. To do it, we call to the .raw()
method on the bodyParser
object imported at the top of our file (a library that offers a collection of format-specific functions for formatting HTTP request body data).
To it, we pass an options object saying that we want to allow any */*
data type and set the request body size limit to 50mb
. This ensures that a payload of any size can get through without triggering any errors (feel free to play with this according to your own needs).
Finally, because we expect the .raw()
method to return a function, we immediately call that function, passing in the req
, res
, and next
arguments passed to us via Express when it calls our middleware.
With this, we're ready to dig into the actual handlers for our webhooks. First, we need to add the /webhooks/stripe
endpoint we alluded to earlier when adding our endpoint on the Stripe dashboard.
Adding an Express endpoint for receiving webhooks
This one is quick. Recall that earlier, in the Stripe dashboard, we assigned our endpoint to http://tunnel.cheatcode.co/webhooks/stripe
. Now, we need to add that /webhooks/stripe
route in our application and wire it up to the handler code that will parse and receive our webhooks.
/api/index.js
import graphql from "./graphql/server";
import webhooks from "./webhooks";
export default (app) => {
graphql(app);
app.post("/webhooks/:service", webhooks);
};
Above, the function we're exporting is called via our /index.js
file after the middleware()
function. This function is designed to set up the API or routes for our application. By default, in this boilerplate, our API is based on GraphQL. The graphql()
function call we see here is irrelevant, but the app
argument it's receiving is important.
This is the Express app
instance created in our /index.js
file. Here, we want to call to the .post()
method on that app instance to tell Express we'd like to define a route which receives an HTTP POST request (what we expect to get from Stripe). Here, to keep our code open-ended and applicable to Stripe as well as other services, we define our route's URL as /webhooks/:service
where :service
is a param that can be swapped with the name of any service (e.g., /webhooks/stripe
or /webhooks/facebook
).
Next, we want to take a look at the function stored in the webhooks
variable we're importing at the top of the file and passing as the second argument to our route.
Adding a webhook handler
The real meat of our implementation will be the handler function we're going to write now. This is where we'll accomplish two things:
- Validating the webhook payload that we receive from Stripe (to ensure the data we're receiving is actually from Stripe).
- Locating and calling the appropriate code (a function) based on the type of webhook (for our example, either
invoice.payment_succeeded
orinvoice.payment_failed
).
To start, we're going to write the validation code using the stripe
package that we installed earlier:
/api/webhooks/index.js
import _ from "lodash";
import settings from "../../lib/settings";
import { stripe } from "./stripe";
const handlers = {
stripe(request) {
// We'll implement our validation here.
},
};
export default async (req, res, next) => {
const handler = handlers[req.params.service];
if (handler) {
res.status(200).send("[200] Webhook received.");
handler(req);
} else {
res.status(200).send("[200] Webhook received.");
}
};
In our previous step, we set up an Express route, passing it a variable webhooks
, a function, as the second argument that's called when a request is made to the URL you define, in this case /webhooks/stripe
.
In the code above, we're exporting a function which takes in three arguments: req
, res
, and next
. We're anticipating these specific arguments as these are what Express will pass to the callback function for a route (in this case, that callback function is the function we're exporting here and importing back in /api/index.js
as webhooks
).
Inside of that function, we need to confirm that the service we're receiving a request for stripe
has a corresponding handler function to support it. This is so that we don't receive random requests from the internet (e.g. someone spamming /webhooks/hotdog
or /webhooks/pizzahut
).
To verify that we _have _ a handler function, above our exported function we've defined an object handlers
and have defined Stripe as a function on that object (a function defined on an object is referred to as a method in JavaScript).
For that method, we expect to take in the HTTP request object passed to our route. Back down in our exported function—the route callback—we determine which handler to call based on the req.params.service
value. Remember, the :service
in our URL can be anything, so we need to make sure it exists first before calling it. To do that, we use JavaScript bracket notation to say "on the handlers
object, try and find a property with a name equal to the value of req.params.service
."
For our example, we'd expect handlers.stripe
to be defined. If that handler
exists, we want to signal back to the original request that the webhook was received and then call that handler()
function, passing in the req
that we want to handle.
/api/webhooks/index.js
import _ from "lodash";
import settings from "../../lib/settings";
import { webhooks as stripeWebhooks, stripe } from "./stripe";
const handlers = {
stripe(request) {
const data = stripe.webhooks.constructEvent(
request.body,
request.headers["stripe-signature"],
settings.stripe.webhookSecret
);
if (!data) return null;
const handler = stripeWebhooks[data.type];
if (handler && typeof handler === "function") {
return handler(data?.data?.object);
}
return `${data.type} is not supported.`;
},
};
export default async (req, res, next) => {
const handler = handlers[req.params.service];
if (handler) {
res.status(200).send("[200] Webhook received.");
handler(req);
} else {
res.status(200).send("[200] Webhook received.");
}
};
Filling out our stripe()
handler function, before we do anything with the webhook we've received from Stripe, we want to ensure that the webhook we're receiving is actually from Stripe and not somebody trying to send us suspicious data.
To do that, Stripe gives us a handy function in its Node.js library—the stripe
package we installed at the beginning of the tutorial—for performing this task: stripe.webhooks.constructEvent()
.
Here, we're importing an instance of stripe
from the file /stripe/index.js
located inside of our existing /api/webhooks
folder (we'll set this up in the next section, so for now we're assuming its existence).
We expect that instance to be an object containing the .webhooks.constructEvent()
function that we're calling to here. That function expects three arguments:
- The
request.body
that we received in the HTTP POST request from Stripe. - The
stripe-signature
header from the HTTP POST request we received from Stripe. - Our
webhookSecret
that we set up and added to our/settings-development.json
file earlier.
The first two arguments are immediately available to us via the HTTP request
(or req
as we've referenced it elsewhere) object we've received from Stripe. For the webhookSecret
, we've imported our settings file as settings
at the top of our file, leveraging the built-in settings loader function in /lib/settings.js
to pick out the correct settings for us based on our current environment (based on the value of process.env.NODE_ENV
, for example, development
or production
).
Inside of constructEvent()
, Stripe attempts to compare the stripe-signature
header with a hashed copy of the received request.body
. The idea here is that, if this request is valid, the signature stored in stripe-signature
will be equal to the hashed version of the request.body
using our webhookSecret
(only possible if we're using a valid webhookSecret
and receiving a legitimate request from Stripe).
If they do match, we expect the data
variable we're assigning our .constructEvent()
call to to contain the webhook we received from Stripe. If our validation fails, we expect this to be empty.
If it is empty, we return null
from our stripe()
function (this is purely symbolic as we don't expect a return value from our function).
Assuming that we did successfully receive some data, next, we want to try and find the webhook handler for the specific type
of event we're receiving from Stripe. Here, we expect this to be available in the type
property on the data
object.
At the top of our file, we're also assuming that our /stripe/index.js
file here in /api/webhooks
will contain an exported value webhooks
which we've renamed as stripeWebhooks
when importing it up top (again, we haven't created this yet—we're just assuming it exists).
On that object, as we'll see in the next section, we expect a property matching the name of the webhook type
we've received (e.g., invoice.payment_succeeded
or invoice.payment_failed
).
If it does exist, we expect it to return a function to us that itself expects to receive the data contained in our webhook. Assuming that it does, we call that handler()
function, passing in data.data.object
—here, using JavaScript optional chaining to ensure that object
exists on the data
object above it, which exists on the data
object we stored the parsed and validated request body from Stripe.
To wrap up, let's take a look at this /api/webhooks/stripe/index.js
file we've been dancing around.
Adding functions to handle specific webhook events
Now, let's see how we intend to get access to the instance of Stripe we alluded to above and handle each of our webhooks:
/api/webhooks/stripe/index.js
import Stripe from "stripe";
import settings from "../../../lib/settings";
import invoicePaymentSucceeded from "./invoice.payment_succeeded";
import invoicePaymentFailed from "./invoice.payment_failed";
export const webhooks = {
"invoice.payment_succeeded": invoicePaymentSucceeded,
"invoice.payment_failed": invoicePaymentFailed,
};
export const stripe = Stripe(settings.stripe.secretKey);
Focusing on the bottom of our file, here we can see the stripe
value where we called the stripe.webhooks.constructEvent()
being intialized. Here, we take the Stripe
function imported from the stripe
package we installed at the start of the tutorial being called, passing in the secretKey
we took from the Stripe dashboard and added to our /settings-development.json
file earlier.
Above this, we can see the webhooks
object we imported and renamed as stripeWebhooks
back in /api/webhooks/index.js
. On it, we have the two event types we'd like to support invoice.payment_succeeded
and invoice.payment_failed
defined, for each passing a function with a name corresponding to the code we want to run when we receive those specific types of events.
For now, each of those functions are limited to exporting a function which console.log()
s the webhook we've received from Stripe. This is where we'd want to take the webhook and make a change to our database, create a copy of the invoice we've received, or trigger some other functionality in our app.
/api/webhooks/stripe/invoice.payment_succeeded.js
export default (webhook) => {
console.log(webhook);
};
That's it! Now, let's spin up a tunnel via the Ngrok tool we hinted at earlier and receive a test webhook from Stripe.
Wrapping up
In this tutorial, we learned how to set up a webhook endpoint on Stripe, obtain a webhook secret, and then securely validate a webhook using the stripe.webhooks.constructEvent()
function. To get there, we set up an HTTP POST route in Express and wired up a series of function to help us organize our webhook handlers based on the type of event received from Stripe.