Published on

Listen to Stripe webhooks with Directus.

Authors

Context

In order to listen to Stripe webhooks, you need to verify that the current request comes from Stripe by checking the signature in the header. Then you can read the body of the request, do what you need to and send a proper response to Stripe.

However, to verify the signature using: stripe.webhooks.constructEvent(request.body, sig, endpointSecret); as mentioned in the documentation, you need to have request.body that is the raw request body. Otherwise you will end up with this error:

Webhook signature verification failed. No signatures found matching the expected signature for payload. Are you passing the raw request body you received from Stripe? https://github.com/stripe/stripe-node#webhook-signing

So you need to create two extension to properly read Stripe webhooks. One is a custom hook that will retrieve the raw body for you. The second is a custom endpoint to listen to Stripe Webhook and act accordingly. So your directus folder might look like:

.
├── extensions                          # All your extensions
    ├── endpoints                       # Listener for the Stripe webhooks
    |   └── stripe-webhook
    |       └── index.js
    └── hooks                           # Your custom hook using a middleware to retrieve the raw request body
        └── stripe-webhook-middleware
            └── index.js

Create a custom hook

To create a custom hook, use the tool to create a Directus extension as mentioned in the documentation.

Then add this to create the hook. The solution comes from here and still works with Directus 9.18.

// index.js
const express = require('express')

module.exports = function registerHook({ init }) {
  init('middlewares.before', async function ({ app }) {
    app.use(
      express.json({
        verify: (req, res, buf) => {
          // Change the path to your endpoint, for example: /stripe-webhook
          if (req.originalUrl.startsWith('/path/to/endpoint')) {
            req.rawBody = buf.toString()
          }
        },
      })
    )
  })
}

This will transform every request going to your endpoint /path/to/endpoint and add a rawBody that Stripe will be able to use.

Create a custom endpoint

Once you have your custom hook, you just have to adapt Stripe code example to read req.rawBody and not req.body.

For example, following the quickstart from Stripe:

export default (router, { services, exceptions }) => {
  const { ItemsService } = services
  const { ServiceUnavailableException } = exceptions

  const stripe = require('stripe')('sk_test_SOMETHINGSECRET')
  const endpointSecret = 'whsec_SOMETHINGSECRET'

  router.post('/', async (req, res) => {
    // MAIN DIFFERENCE WITH STRIPE EXAMPLE
    let event = req.rawBody

    // Only verify the event if you have an endpoint secret defined.
    // Otherwise use the basic event deserialized with JSON.parse
    if (endpointSecret) {
      // Get the signature sent by Stripe
      const signature = req.headers['stripe-signature']
      try {
        event = stripe.webhooks.constructEvent(
          req.rawBody, // MAIN DIFFERENCE WITH STRIPE EXAMPLE
          signature,
          endpointSecret
        )
      } catch (err) {
        console.log(`⚠️  Webhook signature verification failed.`, err.message)
        return res.sendStatus(400)
      }
    }

    // Handle the event
    switch (event.type) {
      case 'payment_intent.succeeded':
        const paymentIntent = event.data.object
        console.log(`PaymentIntent for ${paymentIntent.amount} was successful!`)
        // Then define and call a method to handle the successful payment intent.
        // handlePaymentIntentSucceeded(paymentIntent);
        break
      case 'payment_method.attached':
        const paymentMethod = event.data.object
        // Then define and call a method to handle the successful attachment of a PaymentMethod.
        // handlePaymentMethodAttached(paymentMethod);
        break
      default:
        // Unexpected event type
        console.log(`Unhandled event type ${event.type}.`)
        console.log(`The data object of the event: ${event.data.object}`)
    }

    // Return a 200 response to acknowledge receipt of the event
    res.sendStatus(200)
  })
}