Almost every verification bug traced back to the same root cause: the code signs something other than what the provider signed. A framework that parses JSON and re-serializes it changes whitespace, key order, unicode escaping and number formatting, so the recomputed digest no longer matches. GitHub's own docs are blunt about this: the signature is computed over the payload contents, payloads may contain unicode, and you must treat the body as UTF-8 GitHub's validation guide. Shopify integrations hit the same trap; the reliable pattern is to capture the raw body buffer in middleware and verify against that buffer before any parsing happens a Shopify reliability walkthrough.
The practical rule: keep a raw byte buffer on the request object, verify, and only then parse.
// Express: capture bytes before any JSON parser touches them
app.use(express.json({
verify: (req, res, buf) => { req.rawBody = buf; }
}));
app.post('/webhooks', (req, res) => {
if (!verify(req.rawBody, req.headers)) return res.status(401).end();
// safe to use req.body from here
});
A second trap sits closer to the signing side. Stripe's format packs the timestamp and signature into one header (t=...,v1=...), which means the timestamp itself is inside the signed structure; some providers instead put the timestamp in a separate header, and if you forget to fold that header into the digest, an attacker can change the timestamp without breaking the signature webhooks.fyi on replay prevention. Whichever layout you choose, document the exact byte sequence and make it the only thing your library accepts.



