Webhook security and signature verification

July 23, 2026 · 4 min read

A webhook endpoint is a door on the public internet. Without verification you have no idea whether the request came from Stripe or from someone who guessed your URL – and “it worked in testing” says nothing about that.

The threat, concretely

Your endpoint accepts {"event":"payment.succeeded","amount":9999}. If it unlocks a product based on that, anyone who can POST to your URL gets free products. Signature verification is what turns “someone claims” into “the provider says”.

How HMAC signatures work

The provider gives you a signing secret. On every delivery it computes a hash over the raw request body using that secret and sends it in a header (Stripe-Signature, X-Hub-Signature-256, X-Signature). You compute the same hash and compare. Matching hashes prove two things: the sender knows the secret, and the body wasn't modified.

const crypto = require("crypto");

function verify(rawBody, header, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)            // ← the RAW body, not the parsed object
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(header)
  );
}

The three mistakes almost everyone makes

  • Hashing the parsed body. JSON.stringify(req.body) is not byte-identical to what arrived – key order and whitespace differ. You must keep the raw body (in Express: express.raw() for that route).
  • Comparing with ===. String comparison short-circuits on the first differing character, which leaks timing information. Use a timing-safe comparison.
  • Ignoring the timestamp. Without a freshness check, a captured valid request can be replayed forever. Stripe puts t= in the signature header – reject anything older than a few minutes.

What else protects an endpoint

  • An unguessable URL. Not a replacement for signatures, but it keeps scanners away.
  • HTTPS only – otherwise the payload travels in the clear.
  • IP allow-lists where the provider publishes ranges (GitHub, Stripe do).
  • Rate limiting, so a flood of requests can't take the endpoint down.
  • Treat the payload as untrusted input even after verification – it's still user-influenced data.

How much of this do I need?

It depends entirely on what the webhook triggers. If it grants access, moves money or writes to a database: everything above. If it only makes your phone buzz – as with a Webhooky endpoint, where the worst case is an unwanted notification and deleting the endpoint kills the URL instantly – a long random URL and HTTPS are a proportionate answer. Match the effort to the consequences.

More: webhook best practices · what is a webhook

Get Webhooky

Free for your first 100 notifications – set up your endpoint in two minutes.

Get it on Google Play Download on the App Store