A Stripe webhook is trustworthy only after your endpoint verifies the signature against the exact raw request body. Parsing JSON first and then serializing it again can change the bytes, so a reconstructed string may no longer match the payload Stripe signed.
The three verification inputs
Stripe’s recommended libraries verify the raw request body, the Stripe-Signature header, and the endpoint secret that begins with whsec_. The secret is unique to an endpoint. Test and live destinations have different secrets, and one endpoint’s secret should not be reused for another. Keep it in a server-side secret store; never ship it to a browser or print it in logs.
A handler should read the raw bytes, retrieve the signature header, call the official verification helper, reject a failed check, and return a 2xx response quickly after durable acceptance.
Why the raw body matters
The signature covers the bytes Stripe sent. Middleware can change those bytes before verification sees them. Common causes include automatically parsing JSON, normalizing whitespace, changing character encoding, reordering keys during serialization, or consuming the request stream before the handler reads it.
Framework guidance differs. Some runtimes expose a raw-body buffer beside the parsed object. Others need route-specific parser configuration. The right test is not “does the JSON look the same?” It is “did verification receive the original byte sequence?”
If verification starts failing after a framework upgrade, inspect the request pipeline before rotating secrets. A parser change can break every signature while the secret remains correct.
What the signature header proves
Stripe’s manual verification description builds a signed payload from the timestamp, a period, and the raw JSON payload. It computes an HMAC with SHA-256 and the endpoint secret. The Stripe-Signature header can contain the timestamp and one or more signatures.
Multiple signatures matter during secret rotation. Stripe can keep an old secret active for an overlap period while the new secret is introduced, producing a signature for each active secret. Use the official helper or compare all eligible signatures rather than assuming one value.
Use the official library or a constant-time comparison designed for authentication data. An ordinary early-exit string comparison is not the right primitive for an HMAC.
The timestamp is a replay control
A valid HMAC proves that someone with the endpoint secret signed a particular payload. Without a freshness check, the same signed request could be replayed later.
Stripe’s libraries use a default tolerance of five minutes. They compare the signed timestamp with the server’s current time. Clock accuracy therefore belongs inside the security boundary: synchronize production hosts with a reliable time source and alert on meaningful drift.
Setting the tolerance to zero is not a safe way to disable the check. Stripe warns that zero disables the recency condition. Use the library default unless a measured delivery constraint requires a carefully chosen alternative.
Each automatic retry receives a new timestamp and signature. Normal retries do not require a wider window.
Verification is not deduplication
A request can be authentic and still be delivered more than once. Stripe advises logging processed event IDs so a handler can avoid repeating work. Separate event objects can also describe the same underlying object and event type, so the business operation needs its own idempotency rule.
| Control | Question it answers |
|---|---|
| Signature verification | Did Stripe sign these exact bytes? |
| Timestamp tolerance | Is this signed request recent enough? |
| Event-ID tracking | Have we already accepted this event? |
| Business idempotency | Has the underlying action already been applied? |
For example, a handler may verify an authentic payment_intent.succeeded event twice. Signature verification should pass both deliveries. The processing layer should recognize the repeated event ID and avoid issuing the same entitlement twice.
Our guide to idempotency keys and safe retries covers the wider pattern. For choosing between push delivery and periodic reads, see webhooks vs polling.
Return success before heavy work
Stripe recommends returning a successful response quickly before complex logic that could time out. A practical design is:
- verify the signature and timestamp;
- validate the minimum envelope fields;
- insert the event into a durable queue or inbox with a uniqueness constraint;
- return
2xx; - process the business action asynchronously;
- record the final outcome and retry policy.
The durable insert is the handoff. If it fails, do not return success. If it succeeds, later business processing can retry without keeping the HTTP request open.
The inbox should preserve enough information for an audit: event ID, type, creation time, receipt time, processing state, and the relevant object identifier. Avoid logging full payloads by default when they contain customer data.
Rotate secrets without an outage
Stripe allows a secret roll with an overlap period of up to 24 hours. During that period, multiple secrets are valid and Stripe generates signatures for each active secret.
A safe sequence is:
- add the new secret while retaining the old one;
- deploy and confirm that new deliveries verify;
- allow the planned overlap to cover all instances and regions;
- revoke the old secret;
- remove it from configuration after rollout is confirmed.
Do not rotate blindly in response to a raw-body parsing bug. That adds another moving part while the original failure remains.
A test plan for production failures
Test that the endpoint:
- accepts an official test event with the correct raw body, header and secret;
- rejects the same header with one payload byte changed;
- rejects a signature made with another endpoint’s secret;
- rejects an old timestamp outside the allowed tolerance;
- accepts both active secrets during a planned rotation;
- handles a repeated event ID without repeating the business action;
- returns
2xxonly after durable acceptance; - keeps verification working when other routes use normal JSON parsing.
Test the deployment path too. A unit test that calls the verifier directly will not catch a reverse proxy or middleware that consumes the body first.
When verification fails
Log the category without exposing secrets: missing header, missing raw body, signature mismatch, stale timestamp, or malformed envelope. Include a request correlation ID and endpoint configuration name, not the secret.
Check the endpoint and environment, the current endpoint secret, the raw-body path, server clock, any active secret rotation, and any proxy or middleware transformations.
The core rule is small: verify the raw bytes, signature header and correct endpoint secret before trusting the event. The production system around it still needs a fresh timestamp, durable acceptance and idempotent processing.
Sources and version note
This guide was checked on October 26, 2026 against Stripe’s webhook documentation, including signature verification, replay protection, duplicate events and secret rotation. Framework APIs and SDK interfaces can change; verify the current code sample for the language and endpoint type you use.





