All documentation
Authentication

Webhooks: Verify Signatures in Node.js

Verify PulseGrid webhook signatures in an Express backend without changing the raw request bytes.

JavaScript

Overview

Node.js receivers must calculate the signature from the original raw request body. If middleware parses the JSON first and discards the raw bytes, the signature calculation may use different bytes and fail. Configure the webhook route to receive a Buffer. The signing secret belongs on the backend only.

Setup

1. Store PULSEGRID_WEBHOOK_SECRET in your server environment. 2. Configure express.raw for this route. 3. Validate the timestamp is no older than five minutes. 4. Calculate HMAC-SHA256 over timestamp.rawBody. 5. Use timingSafeEqual only after confirming both buffers have equal lengths. 6. Parse JSON only after verification.

Code example

Webhooks: Verify Signatures in Node.js
import crypto from "node:crypto";
import express from "express";

const app = express();

app.post(
  "/webhooks/pulsegrid",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const timestamp = req.header("X-PulseGrid-Timestamp") || "";
    const supplied = req.header("X-PulseGrid-Signature") || "";
    const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));

    if (!Number.isFinite(age) || age > 300) {
      return res.status(401).json({ error: "Expired request" });
    }

    const digest = crypto
      .createHmac("sha256", process.env.PULSEGRID_WEBHOOK_SECRET)
      .update(Buffer.concat([Buffer.from(timestamp + "."), req.body]))
      .digest("hex");

    const expected = Buffer.from("v1=" + digest);
    const actual = Buffer.from(supplied);
    if (expected.length !== actual.length ||
        !crypto.timingSafeEqual(expected, actual)) {
      return res.status(401).json({ error: "Invalid signature" });
    }

    const event = JSON.parse(req.body.toString("utf8"));
    await handlePulseGridEvent(event);
    return res.status(200).json({ received: true });
  }
);

Notes

Do not put express.json() in front of this webhook route unless you also preserve the original raw body. Return 401 for invalid signatures. Never process an event whose signature could not be verified.