Webhook Signature Verification
Proving an incoming webhook really came from the sender by recomputing its HMAC signature with a shared secret before trusting the body.
See it
What it is
Your webhook URL is public, so anyone who guesses it can POST 'subscription paid' to your server. Signature verification closes that hole. The sender hashes the raw request body (usually plus a timestamp) with a shared signing secret and puts the result in a header: 'Stripe-Signature', 'X-Hub-Signature-256' on GitHub, 'svix-signature' on Clerk and friends. You recompute the same HMAC with your copy of the secret and compare. Every provider builds that signed string differently, so follow their documentation rather than a snippet you found for someone else's API.
Do it as the first thing in the handler, before parsing, before touching the database. Compare with a timing-safe function (crypto.timingSafeEqual in Node), never a plain equality check. Reject anything whose timestamp is more than about five minutes old, otherwise someone who captured a valid payload can replay it forever. Signing secrets are separate from API keys and rotate separately.
The gotcha that eats an entire afternoon: your framework parses the body into an object before you see it, and re-stringifying it changes whitespace or key order, so the hash never matches. You need the exact raw bytes, which in Next.js route handlers means awaiting req.text() and in Express means mounting express.raw() on just that route. Signature checks prove authenticity, not uniqueness, so still deduplicate by event ID because providers retry.
Ask AI for it
Add signature verification to this webhook handler for the provider it actually receives from. Use that provider's official verification helper if one exists; if not, follow its documented canonical string, hash algorithm, header format, and timestamp tolerance exactly instead of assuming HMAC SHA-256 over the body. Read the raw request bytes and verify before anything JSON-parses them, load the signing secret from process.env, accept more than one valid signature so a key rotation does not cause downtime, and compare in constant time with crypto.timingSafeEqual. Reject with 400 before touching the database, then store the event ID and skip events already processed so retries are safe. Write tests for a valid event, a tampered body, a stale timestamp, and a duplicate delivery.