Cryptography

How to Verify Stripe Webhook Signatures with HMAC

9 min read
By
How to Verify Stripe Webhook Signatures with HMAC

Photo by Tima Miroshnichenko from Pexels

When Stripe sends a webhook to your server, anyone on the internet can send a POST request to that same URL. Without signature verification, your server has no way to distinguish a real Stripe event from a fake one. An attacker could send a forged "payment succeeded" event and get your product for free.

Stripe solves this with HMAC-SHA256. Every webhook includes a signature computed with your endpoint's signing secret. Your server verifies the signature before processing the event. If the signature does not match, the request is rejected.

This is straightforward in principle, but there are several details that developers get wrong. Let's walk through the full process.

Understanding the Stripe-Signature Header

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

t=1614602325,v1=5257a869e32369e8352a4f8a3212a3b8...

This header contains two pieces of information:

**t** is the timestamp when the webhook was sent. Stripe includes this so you can reject old webhooks that are being replayed. Without a timestamp check, an attacker who captures a valid webhook could resend it later.

**v1** is the HMAC-SHA256 signature. It is computed over the string "{timestamp}.{request_body}" using your webhook signing secret as the key.

How Stripe Computes the Signature

The signature is computed as follows:

1. Take the timestamp from the t field. 2. Take the raw request body (the exact bytes that were sent, not a parsed or re-serialized version). 3. Concatenate them as "{timestamp}.{request_body}". 4. Compute HMAC-SHA256 using your signing secret as the key. 5. The result, hex-encoded, is the v1 signature.

The critical detail is that you must use the raw request body. If your web framework parses the JSON body and you re-serialize it, the byte order or whitespace might differ, and the signature will not match. Always work with the raw bytes.

Verifying the Signature

Here is how to verify a Stripe webhook signature in practice:

**Step 1: Parse the header.** Split the Stripe-Signature header on commas to extract the t and v1 values.

**Step 2: Check the timestamp.** Compare the timestamp to the current time. If the difference is more than your tolerance window (Stripe recommends 5 minutes), reject the request. This prevents replay attacks.

**Step 3: Compute the expected signature.** Concatenate the timestamp, a period, and the raw request body. Compute HMAC-SHA256 using your signing secret.

**Step 4: Compare signatures.** Use a constant-time comparison function to compare the expected signature with the v1 value from the header. Do not use a regular string comparison, which is vulnerable to timing attacks. We cover why in our guide on constant-time comparison.

**Step 5: Handle the result.** If the signatures match and the timestamp is within tolerance, process the event. If not, return a 400 status code and do not process the event.

Using the Stripe SDK

The easiest and safest way to verify Stripe webhooks is to use the official Stripe SDK. The SDK handles all of the steps above for you:

const event = stripe.webhooks.constructEvent( rawBody, signature, webhookSecret );

This single call parses the header, checks the timestamp tolerance (default 5 minutes), computes the HMAC, and compares it in constant time. If anything fails, it throws an error. If it succeeds, you get the parsed event object.

The SDK is available for Node.js, Python, Ruby, PHP, Go, Java, .NET, and more. Use it. Manual verification is only necessary if you are working in a language without an official SDK, and even then, you should closely follow Stripe's documentation.

Common Mistakes

**Using the parsed body instead of the raw body.** This is the most common mistake. If your framework parses JSON before your code runs, you need to configure it to also provide the raw body. In Express.js, use express.raw({ type: 'application/json' }) for the webhook route. In Next.js, read the body from the request stream before parsing.

**Skipping the timestamp check.** Even if the HMAC is valid, an old webhook might represent an event that has already been processed. The timestamp check ensures you are handling fresh events. Without it, an attacker who captures a webhook can replay it indefinitely.

**Using regular string comparison.** Comparing signatures with === or == leaks timing information. An attacker can measure how long the comparison takes to determine how many bytes of the signature are correct, then forge the signature one byte at a time. Always use constant-time comparison. The Stripe SDK does this for you automatically.

**Exposing the signing secret.** Your webhook signing secret should be stored in an environment variable, never committed to version control, and never sent to the client. If an attacker gets your signing secret, they can forge valid webhooks. If you suspect the secret has been compromised, rotate it immediately in the Stripe Dashboard.

Why HMAC and Not a Plain Hash?

Stripe uses HMAC rather than a plain hash because a plain hash of the request body could be computed by anyone who sees the body. An attacker could modify the body, recompute the hash, and send both. HMAC requires the secret key, so an attacker cannot produce a valid signature for a modified body.

This is the fundamental difference between integrity and authenticity that we cover in our HMAC vs hash guide. A hash says the body was not corrupted. An HMAC says the body was not tampered with by someone without the key. For webhooks, you need the latter.

Testing Your Verification

The best way to test your webhook verification is to use the Stripe CLI. It can forward real webhook events to your local development server:

stripe listen --forward-to localhost:3000/webhook

This sends real-signed webhooks to your local endpoint using your test-mode signing secret. If your verification code is correct, events will be processed. If something is wrong, you will see signature verification errors.

You can also test failure cases by modifying the request body or the signature header and confirming that your server rejects the request. This is important because rejecting invalid signatures is just as critical as accepting valid ones.

The Bottom Line

Verifying Stripe webhook signatures is not optional. Without it, your webhook endpoint is an open door for anyone who knows the URL. The verification process is simple with the Stripe SDK, but the details matter: use the raw body, check the timestamp, and use constant-time comparison.

If you want to understand the HMAC construction in more detail, try our HMAC generator to compute HMAC-SHA256 signatures with your own keys and messages. And for the broader context of why HMAC is different from a plain hash, read our HMAC vs hash explained post.

Frequently Asked Questions

What is the Stripe webhook signing secret?

The Stripe webhook signing secret is a string that starts with "whsec_" and is found in your Stripe Dashboard under Developers > Webhooks. Each webhook endpoint has its own signing secret. Stripe uses this secret to compute the HMAC-SHA256 signature for every webhook sent to that endpoint, and you use it to verify signatures on your server.

What happens if I do not verify Stripe webhook signatures?

If you do not verify signatures, anyone who knows your webhook URL can send fake POST requests to it. They could forge a "payment succeeded" event and get free products or services without paying. Signature verification is the only way to confirm that a webhook actually came from Stripe.

Why does Stripe include a timestamp in the signature?

The timestamp prevents replay attacks. Without it, an attacker could capture a legitimate webhook, resend it weeks later, and your server would accept it as valid. By checking that the timestamp is within a few minutes of the current time, you reject stale webhooks that are being replayed.

Should I use Stripe SDK or verify the signature manually?

Use the Stripe SDK. The SDK handles signature parsing, timestamp tolerance, HMAC computation, and constant-time comparison correctly. Manual verification is error-prone and the SDK is maintained by Stripe engineers who understand the exact format. Only verify manually if you are in a language without an official SDK.

Try NovelCrypt Tools

Experience military-grade encryption for your sensitive data. Create self-destructing messages, encrypt files, or explore our experimental lab tools.

Explore NovelCrypt