#Webhooks

#Creating a webhook

  1. Go to one of your sites
  2. Go to the "Hooks" tab
  3. Click "Add"
  4. Select type "Web"
  5. Fill in the destination and add a secret (use openssl rand -hex 32)
  6. Save the secret somewhere safe, your consumer application will need it

#Consuming webhooks

Webhooks are delivered with a X-Webhook-Signature header computed as an HMAC using sha256.

To verify the integrity of a webhook:

  1. compute the signature of the request raw body using an HMAC with sha256 and the webhook secret
  2. verify that the computed signature equals the X-Webhook-Signature header content

Reference NodeJS TypeScript implementation:

`
import { createHmac } from 'crypto';

async function verifyWebhookSignature(req: Request, secret: string): Promise<boolean> {
  const signature = req.header('X-Webhook-Signature');
  const { rawBody } = req as any;
  if (!signature || !rawBody || !Buffer.isBuffer(rawBody)) {
    return false;
  }
  const hmac = createHmac('sha256', secret)
    .update(rawBody)
    .digest()
    .toString('hex');
  return hmac === signature;
}

In ExpressJS, the raw body of a request can be obtained as follows:

`
app.use(json({
    verify: (req: any, res, buf) => {
      // for performance, only do this when needed
      if (Buffer.isBuffer(buf) && req.header('X-Webhook-Signature')) {
        // store raw body for signature verification
        req.rawBody = buf;
      }
      return true;
    },
}));

#Example ExpressJS application

`
require('dotenv').config();

const express = require('express');
const bodyParser = require('body-parser');
const createHmac = require('crypto').createHmac;

function signBody(payload, secret) {
  return createHmac('sha256', secret)
    .update(payload)
    .digest()
    .toString('hex');
}

function verifyWebhookSignature(req, secret) {
  const signature = req.header('X-Webhook-Signature');
  const { rawBody } = req;
  if (!signature || !rawBody || !Buffer.isBuffer(rawBody)) {
    return false;
  }
  return signBody(rawBody, secret) === signature;
}

const app = express();

app.use(bodyParser.json({
  verify: (req, res, buf) => {
    // for performance, only do this when needed
    if (Buffer.isBuffer(buf) && req.header('X-Webhook-Signature')) {
      // store raw body for signature verification
      req.rawBody = buf;
    }
    return true;
  },
}));

app.post('/', (req, res) => {
  const isValid = verifyWebhookSignature(req, process.env.WEBHOOK_SECRET);

  if (!isValid) {
    res.status(400).send({
      message: 'signature is invalid',
    });
    return;
  }

  res.status(204).send();

  console.log(JSON.stringify(req.body, null, 2));
});

const port = 8000;
app.listen(port, () => console.log(`Listening on ${port}`));