← Back to Blog
Guide stripe webhooks security hmac signature

How to Verify Stripe Webhook Signatures (and Why Yours Might Be Failing)

Signature verification is the single most common reason Stripe webhooks fail in production. Here's exactly how the Stripe-Signature header works, the four mistakes that break verification, and how to get it right.

JW

Jason Warner

June 2, 2026

How to Verify Stripe Webhook Signatures (and Why Yours Might Be Failing)

If your Stripe webhooks are returning 400s or you're seeing "signature verification failed" in your logs, you are in good company. This is the single most common webhook problem I get asked about, and it's almost always one of four mistakes.

Let's start with how the signature actually works, because most of the failures come from not understanding the scheme.

What's Actually in the Stripe-Signature Header

When Stripe sends a webhook, it includes a header that looks like this:

Stripe-Signature: t=1492774577,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

There are two parts: t is the timestamp the event was signed, and v1 is an HMAC-SHA256 signature. To verify it, you recompute the signature yourself and check that it matches.

The signed payload is not just the JSON body. It's the timestamp, a period, and the raw body, concatenated:

signed_payload = t + "." + raw_request_body

You then compute HMAC-SHA256(signed_payload, webhook_signing_secret) and compare it to v1.

The Four Mistakes That Break Verification

1. Reading the parsed body instead of the raw bytes

This is the big one. Most web frameworks parse the request body into a JSON object before your handler sees it. When you re-serialize that object to compute the signature, the bytes are different — key order changes, whitespace changes, unicode escaping changes — and the HMAC no longer matches.

You must verify against the exact raw bytes Stripe sent.

// ASP.NET Core: read the raw body before model binding touches it
Request.EnableBuffering();
using var reader = new StreamReader(Request.Body, leaveOpen: true);
var rawBody = await reader.ReadToEndAsync();
Request.Body.Position = 0;

var stripeEvent = EventUtility.ConstructEvent(
    rawBody,
    Request.Headers["Stripe-Signature"],
    webhookSecret);

2. Using the wrong signing secret

The signing secret (whsec_...) is per webhook endpoint, not your API key. If you have multiple endpoints — test mode, live mode, a staging endpoint — each has its own secret. Mixing them up produces a perfectly valid-looking signature that never matches.

3. A proxy or middleware rewrote the body

If there's a gateway, WAF, or body-rewriting middleware between Stripe and your handler — anything that re-encodes, pretty-prints, or strips characters — the bytes change and verification fails. This is sneaky because it works locally and breaks only in production.

4. Clock skew on the timestamp check

Stripe's libraries reject events whose timestamp is too far from the current time (default tolerance is five minutes) to prevent replay attacks. If your server's clock is wrong, valid events get rejected. Check NTP.

Verifying Without the Stripe SDK

If you're verifying manually (no SDK), the logic is:

var parts = signatureHeader.Split(',')
    .Select(p => p.Split('=', 2))
    .ToDictionary(p => p[0], p => p[1]);

var signedPayload = $"{parts["t"]}.{rawBody}";
var expected = Convert.ToHexString(
    new HMACSHA256(Encoding.UTF8.GetBytes(secret))
        .ComputeHash(Encoding.UTF8.GetBytes(signedPayload)))
    .ToLowerInvariant();

// Constant-time comparison — don't use ==
var valid = CryptographicOperations.FixedTimeEquals(
    Encoding.UTF8.GetBytes(expected),
    Encoding.UTF8.GetBytes(parts["v1"]));

Note the constant-time comparison. A naive == comparison can leak timing information; use FixedTimeEquals.

Letting the Infrastructure Handle It

The raw-body problem is the root of most signature failures, and it fights against how web frameworks want to work. This is one of the reasons Bluejay Relay verifies signatures at the edge, before any parsing happens.

You configure the signing secret once per integration, and Bluejay Relay verifies the Stripe-Signature header against the untouched raw bytes the moment the request arrives. Verified events are captured, logged, and forwarded to your application; invalid ones are rejected before they ever reach your code. Bluejay Relay supports the same scheme for GitHub (X-Hub-Signature-256), Shopify, and Twilio, so you configure verification the same way regardless of provider.

Your application then receives only trusted, already-verified events — and you never have to fight your framework's body parser again.


Tired of debugging signature verification? Try Bluejay Relay free — signature verification for Stripe, GitHub, Shopify, and Twilio is built in, no code required.

#stripe #webhooks #security #hmac #signature

Stop losing Stripe webhooks.

Bluejay Relay retries failed deliveries automatically with backoff and a dead-letter queue, so a momentary outage never costs you an event. Free to start, no card.