A payment webhook arrives with a valid signature. Your application has already acted on the same event. Should it fulfill the order again? The signature answers who signed the delivery; the application’s processing record answers whether that delivery still requires an action.
Stripe’s webhook documentation makes these responsibilities distinct. It describes signature verification, a signed timestamp to limit replay, and separate handling for duplicate events. Together they offer a useful way to review a payment integration without treating authentication as the whole workflow.
Verify the bytes that actually arrived
Stripe sends events to a registered HTTPS endpoint. Its signature verification needs the raw request body, the signature header and the endpoint’s signing secret. The signature troubleshooting guide explains why reconstructing JSON can fail verification: whitespace, encoding or property order may differ from the bytes Stripe signed.
That creates a boundary in the application’s request handling. A framework’s body parser can be useful elsewhere while being inappropriate before this verification step. Preserve the raw body until the vendor’s verification function has checked it, and keep the endpoint secret separate from unrelated API credentials.
This is also why a failed signature check should be investigated before changing the business logic. A parser or wrong endpoint secret can prevent a legitimate delivery from passing authentication.
How to verify a webhook signature, step by step
Stripe recommends its official libraries, which take the raw body, the Stripe-Signature header and the endpoint secret. Its documentation also describes the manual check, which shows what the libraries do:
- Split the header.
Stripe-Signatureholds a timestamp with at=prefix and one or more signatures with a scheme prefix. Keeptandv1, the only valid live scheme, and ignore the others to prevent downgrade attacks. - Build the signed payload. Concatenate the timestamp, a
.character and the raw request body. - Compute the expected signature. Use HMAC with SHA-256, with the endpoint’s signing secret as the key and the signed payload as the message.
- Compare, then check the age. Compare the expected value with each received
v1signature using a constant-time comparison, then check that the timestamp is within your tolerance.
GitHub uses the same building block with a different header. Its guide to validating deliveries says the X-Hub-Signature-256 header carries an HMAC hex digest of the payload, prefixed with sha256=, computed with the webhook’s secret token. GitHub warns against a plain == comparison and recommends a constant-time function such as crypto.timingSafeEqual. The older X-Hub-Signature header uses HMAC-SHA1 and is kept only for legacy purposes.
Why “webhook signature verification failed” appears
Stripe’s troubleshooting page says this error means at least one of the three inputs is wrong, and it names the endpoint secret as the most common cause. Secrets start with whsec_, but the Stripe CLI and a Dashboard endpoint each have their own, and test and live modes use different secrets for the same endpoint. The other usual causes are a framework that parsed or re-encoded the body before verification, and code that extracted the header incorrectly. GitHub’s list adds a missing secret (no signature header is sent without one), the wrong header or algorithm, and a proxy or load balancer that modified the payload.
A signed timestamp limits replay
An attacker may resend a captured payload with its valid signature. Stripe addresses this by including a timestamp in the signed material. Changing that timestamp invalidates the signature, while checking its age lets the receiver reject an otherwise valid delivery that is too old.
Stripe’s webhook guidance documents a default tolerance of five minutes in its libraries. It warns that a tolerance of zero disables the recency check. A timestamp test also depends on a receiver whose clock is reasonably accurate.
The tolerance applies to the delivery’s signature timestamp, not to an instruction to reject every business event created more than five minutes ago. Stripe creates a new signature and timestamp when it retries an event. An older event can therefore arrive in a new, authenticated delivery.
Retries remain legitimate duplicates
Network failures make repeated delivery a normal operating condition. A sender may retry after an endpoint fails to return a successful response, even when the application started work during the first attempt.
Stripe recommends recording processed event IDs and avoiding repeated processing of already-recorded events. It also notes that some duplicate business events have distinct Event objects; in those cases, the underlying object’s ID and event type help identify the repetition.
These checks answer different questions:
| Check | Question it answers |
|---|---|
| Signature verification | Does the payload match what was signed with this endpoint secret? |
| Signed timestamp | Is this delivery recent enough under the verification policy? |
| Event processing record | Has this event already been handled? |
| Business-object check | Would this action repeat an operation already completed? |
A design review should follow the action as far as the order, account or ledger record that changes. A duplicate detected after fulfillment has already run is too late to protect that side effect.
Delivery order is another independent problem
Stripe does not guarantee that events arrive in the order they were generated. A subscription workflow may receive an invoice-related event before a preceding subscription event. Its documentation recommends retrieving missing objects through the API when needed.
It also warns against using the event creation timestamp as an ordering or deduplication key. Distinct events can share a timestamp. An authenticated, recent delivery may still arrive out of the sequence your interface expects.
For the product team, the implication is concrete: “payment received” should reflect the authoritative business state, rather than whichever event happened to reach a worker first. This is an architectural interpretation of the documented delivery behavior, not a claim that a particular integration has been tested.
Review the boundary before the next release
Trace one incoming event through raw-body handling, verification, the processing record and the final action. Then consider a retry, an old captured delivery and a pair of out-of-order events. Each scenario exercises a different boundary.
The questions are whether the same bytes reach verification, whether the clock-based check stays enabled, and whether a repeated delivery can cause a repeated business operation. A single “webhook verified” log entry cannot answer all three.
The access boundary is also examined in MCP authorization, where a tool request and permission to use the tool remain separate.
Reporting & Sources
Stripe webhook documentation and signature verification troubleshooting, reviewed October 8, 2026, with the manual-verification steps and troubleshooting list rechecked October 10, 2026; GitHub: validating webhook deliveries, reviewed October 10, 2026. The application-review implications above are analysis based on those documented behaviors.





