Thuta Learning
IntermediateWeb Developmentintermediate

Webhook Security: Signature Verification

What you'll walk away with

  • Explain the core ideas behind Webhook Security: Signature Verification
  • Read the diagram and trace how a request, response, or event flows through the system
  • Explain how this applies to a real API integration you might build

Build the mental model

Your webhook endpoint is a public URL. Anyone on the internet who discovers or guesses that URL can send it a POST request shaped exactly like a real event. Nothing about the URL itself proves the request came from your provider.

The fix is signature verification. The provider computes an HMAC signature over the request body using a shared secret. Your server recomputes the same HMAC and compares — matching means accept, mismatched means reject immediately with a 401.

  • Only accept webhooks over HTTPS so signatures and payloads can't be read or altered in transit.
  • Consider checking a timestamp in the request and rejecting anything too old to prevent replay attacks.

Signature verification is not optional hardening — it is the one thing standing between your endpoint and anyone on the internet pretending to be your payment provider.

Webhook Signature
A cryptographic hash computed over a webhook request's body using a shared secret, sent in a request header so the receiver can confirm the request wasn't forged or altered.
HMAC
Hash-based Message Authentication Code — a way to combine a secret key with a message using a hash function so that only someone who knows the key can produce a valid signature for that message.
text
WEBHOOK SIGNATURE VERIFICATION FLOW
-----------------------------------
-----
Incoming POST request with body + X-Signature header
        |
        v
Compute HMAC-SHA256(body, shared_secret) on your server
        |
        v
Compare computed signature to X-Signature header value
        |
   +----+----+
   |         |
 match     no match
   |         |
   v         v
ACCEPT     REJECT
process    401 Unauthorized
event      (do not process)

Connect it to a real scenario

The code below uses Node's built-in crypto module to do exactly what happens in production — no simulation. computeSignature runs HMAC-SHA256 and returns the hex digest.

verifyWebhook recomputes the signature independently and compares it to the header, using crypto.timingSafeEqual rather than a plain equality check to avoid leaking timing information.

The script runs the check twice: once with the genuine signature, once with a deliberately tampered one. Run it yourself and confirm the genuine one is accepted and the tampered one is rejected.

Never trust a request just because it reached your webhook URL

Reaching your endpoint proves nothing about who sent it. Always verify the signature before reading or acting on the body — treat every unverified request as hostile until proven otherwise.

Try the working example

javascript
const crypto = require("crypto");

const sharedSecret = "whsec_test_12345";
const payload = JSON.stringify({ type: "payment.completed", data: { orderId: "ord_482", amount: 4200 } });

function computeSignature(body, secret) {
  return crypto.createHmac("sha256", secret).update(body).digest("hex");
}

function verifyWebhook(body, secret, signatureHeader) {
  const expected = computeSignature(body, secret);
  const isValid = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
  return isValid ? "ACCEPTED: signature matches" : "REJECTED: signature mismatch (401)";
}

const genuineSignature = computeSignature(payload, sharedSecret);
const tamperedSignature = genuineSignature.slice(0, -4) + "0000";

console.log("Payload:", payload);
console.log("Genuine signature:", genuineSignature);
console.log("Tampered signature:", tamperedSignature);
console.log("Check with genuine signature ->", verifyWebhook(payload, sharedSecret, genuineSignature));
console.log("Check with tampered signature ->", verifyWebhook(payload, sharedSecret, tamperedSignature));
You should see
Payload: {"type":"payment.completed","data":{"orderId":"ord_482","amount":4200}}
Genuine signature: 40cc1dccb8401a94ef06c53b4a300a0112a6df9d8cf6f06962044edfadb37696
Tampered signature: 40cc1dccb8401a94ef06c53b4a300a0112a6df9d8cf6f06962044edfadb30000
Check with genuine signature -> ACCEPTED: signature matches
Check with tampered signature -> REJECTED: signature mismatch (401)

5-minute try-it

Add a checkTimestamp function that rejects any webhook whose included timestamp is more than 5 minutes old, and wire it into verifyWebhook before the signature check.

One important caution

Comparing signatures with a plain === or string equality check instead of a timing-safe comparison function.

Verifying the signature against a re-serialized or reformatted body instead of the exact raw bytes the provider signed.

HMAC — WikipediaAPI Integration & Webhooks

Easy traps

  • Comparing signatures with a plain === or string equality check instead of a timing-safe comparison function.
  • Verifying the signature against a re-serialized or reformatted body instead of the exact raw bytes the provider signed.
  • If you haven't taken the API Tutorial yet, it's worth finishing that first -- this course doesn't re-teach REST/HTTP/auth basics, it builds on top of them with webhooks, testing, reliability, and integration architecture.

Exercise

Add a checkTimestamp function that rejects any webhook whose included timestamp is more than 5 minutes old, and wire it into verifyWebhook before the signature check.

You'll know it worked when: Payload: {"type":"payment.completed","data":{"orderId":"ord_482","amount":4200}} Genuine signature: 40cc1dccb8401a94ef06c53b4a300a0112a6df9d8cf6f06962044edfadb37696 Tampered signature: 40cc1dccb8401a94ef06c53b4a300a0112a6df9d8cf6f06962044edfadb30000 Check with genuine signature -> ACCEPTED: signature matches Check with tampered signature -> REJECTED: signature mismatch (401)

Webhook Security: Signature Verification | Thuta Learning