JSON.stringify(req.body) isn't the bytes they signed. Parsing and re-serializing changes whitespace and key order. You need the raw body:
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const expected = crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
const given = String(req.headers["x-signature"] ?? "");
const ok = given.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
if (!ok) return res.sendStatus(401);
const event = JSON.parse(req.body.toString("utf8"));
...
});Register this route before the global express.json(), or that middleware eats the body first. And please don't remove the check, without it anyone can POST fake events.