Ask Anvil

Answers to questions about automating PDFs, e-signatures, Webforms, and other paperwork problems.
E-signatures
Categories

Why does my webhook signature check fail even though the secret is correct, and how do I fix it?

The fix: verify against the raw bytes

const express = require("express");
const crypto = require("crypto");

const app = express();

// Keep the exact bytes the sender signed.
app.use(
  express.json({
    verify: (req, res, buf) => {
      req.rawBody = buf;
    },
  })
);

app.post("/webhooks/signing", (req, res) => {
  const expected = crypto
    .createHmac("sha256", process.env.WEBHOOK_SECRET)
    .update(req.rawBody)
    .digest("hex");

  const received = String(req.get("x-signature") || "");
  const a = Buffer.from(expected);
  const b = Buffer.from(received);

  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.status(401).send("bad signature");
  }
  res.sendStatus(200);
});

module.exports = app;

Why it fails

Most webhook senders compute an HMAC over the exact bytes of the request body. If you parse the JSON and then call JSON.stringify(req.body) to rebuild the string, you get a different byte sequence: key spacing, whitespace, and Unicode escape sequences versus literal characters can all change. One different byte produces a completely different digest, so the check fails even with the right secret. In a local test, a payload with extra spacing and an escaped accented character verified correctly against the raw buffer and failed against the re-serialized object.

Caveats

Register the capturing parser before anything else touches the body. If another middleware parses it first, req.rawBody is never set. Also check your provider's docs for the header name, the encoding (hex or base64), and whether a timestamp is part of the signed string, since the x-signature header and hex digest above are placeholders.

Compare lengths before calling crypto.timingSafeEqual. In Node 22 it throws ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH when the two buffers differ in length, which would turn a bad request into a 500 instead of a 401.

Back to All Questions

The fastest way to build software for documents

Anvil Document SDK is a comprehensive toolbox for product teams launching document flows where PDF filling, signing, and complex conditional scenarios are necessary.
Explore Anvil
Anvil Webforms
Marketing Mode