I wrote a small Node checker (plain JS, no build step) that replays saved webhook deliveries and verifies the HMAC-SHA256 before anything downstream trusts them. Two of three pass. evt_2044 from billing-partner fails, but evt_2051 from the same partner passes with the same secret.
$ node main.js
staging evt_1001 ok
billing-partner evt_2044 FAIL (digest)
billing-partner evt_2051 ok
Both secrets were rotated last week, so I first blamed a stale secret. But then evt_2051 wouldn't have passed either. The only difference I can see is the customer name in 2044: Zoë Ramírez, with accents.
So I suspected utf8 vs latin1 somewhere in how the payload goes into createHmac. I tried changing the encoding passed to update(). No difference at all.
The core of lib/verify.js:
const body = JSON.parse(delivery.raw);
// sign the payload, same as the partner docs describe
const payload = JSON.stringify(body);
const expected = crypto
.createHmac("sha256", secret)
.update(payload, "utf8")
.digest("hex");
The partner docs say the signature is over "the JSON payload" (I'm fairly sure), so I don't see what else I should be hashing. What am I missing? And why only that one event?
(The toleranceSeconds replay check is a TODO, ignore it.)