Skip to main content
You should ensure that webhooks you received were sent by BundleUp. You can do this by verifying the webhook request signature and timestamp. BundleUp sends a BundleUp-Signature HTTP header with every webhook request. This header contains a hex-encoded HMAC-SHA256 signature of the raw body contents, signed using the webhook’s signing secret. You can find the signing secret on the webhook’s detail page. The parsed JSON body has a webhookTimestamp field with a UNIX timestamp, in milliseconds, indicating the time when the webhook was sent. We recommend that you verify it’s within a minute of the time your system sees it to guard against replay attacks. To verify the webhook, you need to compute the signature of the request body using the webhook’s signing secret and compare it against the BundleUp-Signature header. It’s strongly recommended to use raw request body rather than restringifying a parsed JSON body, otherwise the signature may differ. Once the signature has been validated, check to ensure that the webhook timestamp is reasonably current before processing the request:
// Express example:
const crypto = require('node:crypto');
const express = require('express');

const BUNDLEUP_WEBHOOK_SECRET = process.env.BUNDLEUP_WEBHOOK_SECRET;

function verifySignature(headerSignatureString, rawBody) {
  if (typeof headerSignatureString !== 'string') {
    return false;
  }
  const headerSignature = Buffer.from(headerSignatureString, 'hex');
  const computedSignature = crypto
    .createHmac('sha256', BUNDLEUP_WEBHOOK_SECRET)
    .update(rawBody)
    .digest();

  return crypto.timingSafeEqual(computedSignature, headerSignature);
}

const app = express();

app.post(
  '/webhook',
  express.json({
    verify: (req, _res, buf) => {
      // Capture the raw body for signature verification.
      req.rawBody = buf;
    },
  }),
  (req, res) => {
    if (!verifySignature(req.get('bundleup-signature'), req.rawBody)) {
      return res.sendStatus(401);
    }

    if (Math.abs(Date.now() - req.body.webhookTimestamp) > 60 * 1000) {
      // Reject any webhooks not within 60 seconds of the current time to prevent replay attacks.
      return res.sendStatus(401);
    }

    try {
      // ... Handle verified webhook ...
      return res.sendStatus(200);
    } catch (err) {
      // Indicate to Linear that there was a server error so the webhook is retried later.
      return res.sendStatus(500);
    }
  },
);

app.listen(8080, () => console.log('Serving on port 8080'));