Verifying Webhook Signatures with HMAC-SHA256
For incoming webhooks, the signature tells your server whether the request came from the expected sender. The provider includes an HMAC-SHA256 signature computed from the request body and a shared secret; your server recomputes the HMAC with the same secret and compares the two values.1 A match confirms the payload arrived intact and originated from the sender - not from an attacker forging the request. That order matters.
The verification pattern is the same across providers: receive the raw HTTP body bytes before parsing, retrieve the signature header, compute HMAC-SHA256 with your shared secret, and compare with a timing-safe equality function.2 Stripe warns that any framework manipulation of the raw body causes verification to fail; JSON parsing can change whitespace or key order enough to produce a different byte sequence.3
How webhook signature verification works
HMAC-SHA256 combines a secret key with message content in a standardized way defined by RFC 2104.1 GitHub computes the hash signature from your webhook secret token and payload contents, sends it in the X-Hub-Signature-256 header, and prefixes the value with sha256=.2 Stripe signs events with the Stripe-Signature header and requires the raw request body string; framework changes such as whitespace edits, key reordering, JSON conversion, or encoding changes cause verification to fail.3 Consequently, your server must preserve the exact body bytes from the network layer through to the comparison, without parsing or re-serialization in between. Keep the verifier close to the network boundary, where the original bytes and headers remain observable before your framework rewrites them.
Building the expected signature before business logic
Do the signature check before dispatching work to your application layer. If the signature fails, return an authentication error and avoid database writes, queue jobs, or notifications. This ordering keeps forged requests from creating partial side effects even when they reach the edge of your service. Moving the verification step as close to the HTTP handler as possible means the raw body bytes and headers are still in their original form, which avoids the subtle bugs that happen when middleware rewrites the request before your verifier sees it.
You limit the blast radius of a bad request by rejecting it at the edge before it reaches a database write, a queued job, or a notification. A forged webhook that fails verification should never create a side effect, because doing so lets an attacker trigger actions without a valid signature. CapyToolkit's Hash Generator can inspect HMAC-SHA256 output locally while you build the verifier that enforces this ordering.
Security considerations
Timing attacks are a real risk in HMAC verification. Python's hmac documentation warns that comparing a digest with an externally supplied value using == can increase timing-attack exposure, and recommends compare_digest for cryptographic verification.4 A timing-safe comparison keeps verification focused on whether the complete expected value matches, rather than exposing partial-match timing through ordinary equality checks.
Handling failed verification without leaking details
Return the same generic response for missing headers, malformed signatures, clock drift, and HMAC mismatches. Detailed errors can help an attacker learn which part of the protocol is wrong. Log the provider, event ID, and signature header name internally, but keep the client-facing response short and consistent. A single HTTP 401 response with no body tells the caller nothing about whether the key was wrong, the header was missing, or the timestamp was stale, which forces an attacker to guess blindly rather than iterate against a specific failure mode.
Provider-specific patterns
Provider protocols differ around the same HMAC-SHA256 core. Stripe includes a timestamp in the Stripe-Signature header and uses that timestamp as part of replay protection, so applications should reject timestamps outside the accepted window instead of accepting any valid signature.5 Verify the exact header name, signature format, timestamp rules, and signed payload construction in each provider's documentation before implementing.
Testing provider-specific examples before deployment
Run your verifier against sample payloads from the provider documentation before accepting live traffic, which is the cheapest way to confirm a sample payload hashes to Stripe's signature rather than adding temporary debug logging to a webhook handler. Stripe and GitHub format their signatures differently, so a helper that works for one provider may fail the other even though both use HMAC-SHA256. Provider examples give you a safe baseline before you add production logging and alerting. Write automated tests that replay these documented examples on every CI run, so a future refactor that accidentally changes the signed payload construction fails immediately rather than silently breaking webhook processing in production.
For language-specific framework integrations, raw body access differs
Framework integrations differ in how they expose raw request bytes to your application code, and the differences matter because even a single whitespace change in the serialized payload invalidates the HMAC signature. The important rule is to attach raw-body middleware before JSON parsing, or read the raw request body directly before any parser rewrites it. If the framework has already parsed and re-serialized the payload, your code computes HMAC over new bytes and verification fails. In Django, for example, you read the raw body from request.body before accessing request.POST; in Rails, request.body.read gives you the original bytes before the params parser runs.
Document which middleware must run first and add a test that fails if JSON parsing happens before verification. The route contract should name the raw-body middleware, the header used for the signature, and the exact comparison function. That checklist prevents future framework changes from quietly breaking webhook authentication, and it gives new team members a clear ordering constraint to follow when they add middleware to the stack.
When to use this
Verify every incoming webhook before processing its payload. You should implement signature verification for any webhook that triggers state changes - order fulfillment, payment processing, deployment triggers, or user account modifications - where acting on a forged request would cause harm.
Examples
GitHub webhook verification (Node.js)
Request body (raw Buffer): {"action":"opened","number":42,...}
X-Hub-Signature-256 header: sha256=abc123... Computed: sha256=abc123... crypto.timingSafeEqual passes → payload is authentic.
See the HMAC-SHA256 in Node.js guide for the complete Express middleware implementation.
Stripe webhook verification
Stripe-Signature header: t=1683000000,v1=abc123...
Raw body: {"id":"evt_...","type":"payment_intent.succeeded",...} Signed payload: "1683000000.{raw_body}"
Computed HMAC matches v1 value → event is genuine. Stripe's SDK handles header parsing and HMAC computation automatically via stripe.webhooks.constructEvent().
- 1.
H. Krawczyk, M. Bellare, and R. Canetti, "HMAC: Keyed-Hashing for Message Authentication," RFC 2104, IETF, February 1997. https://datatracker.ietf.org/doc/html/rfc2104
- 2.
GitHub, “Validating webhook deliveries,” GitHub Docs, accessed June 2026. https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries
- 3.
Stripe, “Resolve webhook signature verification errors,” Stripe Documentation, accessed June 2026. https://docs.stripe.com/webhooks/signature
- 4.
Python Software Foundation, “hmac - Keyed-Hashing for Message Authentication,” Python 3.10 documentation, March 2026. https://docs.python.org/3.10/library/hmac.html
- 5.
Stripe, “Receive Stripe events in your webhook endpoint,” Stripe Documentation, accessed June 2026. https://docs.stripe.com/webhooks#verify-webhook-signatures-with-official-libraries