A webhook endpoint is a public URL that accepts POST requests and changes order status when they arrive. Anyone who finds that URL can send it a request that looks like a “payment succeeded” event. That is why developers verify payment webhook signatures: the provider signs each message with a secret or a private key, and the receiver checks the signature before it trusts a single field.
Stripe, Adyen and PayPal all sign their webhooks, but they sign different things in different ways. The differences decide what a passing check actually proves, and where integrations usually break.
Three schemes side by side
| Where the signature arrives | What is signed | Algorithm and key | |
|---|---|---|---|
| Stripe | Stripe-Signature header | Timestamp, a period, then the raw request body | HMAC with SHA-256, endpoint signing secret |
| Adyen (standard webhooks) | hmacSignature inside additionalData | Eight named fields joined with colons | HMAC with SHA-256, hex HMAC key per endpoint |
| PayPal | paypal-transmission-sig header | Transmission ID, transmission time, webhook ID and a CRC32 of the raw body | SHA256withRSA, PayPal’s certificate |
Stripe puts a timestamp (t=) and one or more signatures in a single header, according to its webhook documentation. Only the v1 scheme is a live signature; Stripe adds a fake v0 to test events and tells developers to ignore every scheme other than v1 to prevent downgrade attacks. To check it by hand, a receiver joins the timestamp, a . and the body, computes an HMAC-SHA256 with the endpoint’s secret, and compares the result with each v1 value using a constant-time comparison.
Adyen signs its standard webhooks differently. Its HMAC verification guide has the receiver build a string from pspReference, originalReference, merchantAccountCode, merchantReference, the amount value, currency, eventCode and success, in that order, with empty fields left as empty strings. The hex key is converted to binary, the HMAC-SHA256 result is Base64-encoded, and the outcome must match hmacSignature. Some less common Adyen webhooks instead carry the signature in an hmacsignature header, calculated over the body exactly as received.
PayPal uses public-key signatures. Its webhook integration guide builds the message transmissionId|timeStamp|webhookId|crc32 from two headers, the ID PayPal assigned when the listener URL was subscribed, and a decimal CRC32 checksum of the raw body. The receiver downloads the certificate named in paypal-cert-url, caches it, and checks paypal-transmission-sig against the message. PayPal also offers a postback route: send the headers and event to its verify-webhook-signature endpoint and let PayPal do the check. It calls self-verification the preferred method because it avoids an extra API call and its latency.
What a passing check proves
All three signatures show that the message came from the provider and that the signed content was not altered in transit. They do not prove the same amount.
Stripe and PayPal include a time in the signed string, so a receiver can reject an old message that an attacker captured and replayed. Stripe’s libraries apply a default tolerance of five minutes between the signed timestamp and the receiver’s clock. Stripe warns against a tolerance of 0, which turns the recency check off, and recommends keeping server clocks in sync with NTP. Each retry gets a fresh timestamp and signature, so a slow retry does not fail the window. PayPal’s guide puts the transmission time in the signed message but does not state a tolerance, which leaves the window to the receiver.
None of the eight fields in Adyen’s standard signed string is a delivery time. The signature tells a receiver that those fields are authentic, not how old the message is or whether anything outside those fields changed. Adyen’s webhook security page recommends a second layer on top: OAuth 2.0 for standard webhooks, which it calls a much safer option than basic authentication, and basic authentication over HTTPS for other webhook types. In practice the safe reading is to treat the eight signed fields as the facts and to confirm anything else through the API before acting on it.
Where verification usually fails
A parsed body. Stripe and PayPal both check bytes, not JSON. Stripe’s troubleshooting page lists frameworks that add or strip whitespace, reorder keys or change encoding, and in Express the webhook route has to come before express.json() runs. PayPal gives the same warning for its CRC32 and for postback: parsing the body and serializing it again can fail verification. Adyen’s header-signed webhooks have the same rule, while its standard webhooks depend on reading the eight fields exactly as sent.
The wrong secret. Stripe says the most common error is using a secret from a different endpoint. The Stripe CLI and a Dashboard endpoint each issue their own whsec_ secret, and they are not interchangeable. Adyen ties each HMAC key to one endpoint and requires a new key when an integration moves from test to live. PayPal’s mock events from the webhook simulator use the literal string WEBHOOK_ID as the webhook ID, and postback verification does not work for them.
Key rotation. When a Stripe endpoint secret is rolled, the previous secret can stay active for up to 24 hours, and Stripe sends one signature per active secret during that time. A receiver that reads only the first v1 value can reject valid events. Adyen says a new HMAC key takes time to propagate through its infrastructure, so receivers should keep accepting messages signed with the previous key for a while.
After the check passes
A verified message can still arrive twice. Stripe suggests logging processed event IDs, does not guarantee delivery order, and retries undelivered live-mode events for up to three days. Adyen identifies duplicates by matching eventCode and pspReference and tells receivers to use the latest one. PayPal retries a failed delivery up to 25 times over three days. Stripe also shapes each event’s payload by the API version set on the account when the event occurred; our Stripe API record tracks the current version.
Adyen’s handling guide lays out an order that suits all three providers: verify, store the message, return a 2xx response, then run business logic. Adyen marks a webhook as failing if it gets no successful response within 10 seconds, and Stripe asks for a quick 2xx before any complex work. The same discipline applies to outgoing calls; our explainer on idempotency keys in a payment API covers retries in the other direction.
The official libraries handle much of this. Stripe’s constructEvent() and its equivalents, Adyen’s HMAC validators and PayPal’s sample listener all exist so that teams do not have to reimplement string building and comparison. A custom check is worth writing only when a team can test it against each provider’s published examples.
Questions readers ask
Is HTTPS enough to secure a webhook endpoint?
No. HTTPS protects the connection, but anyone can open an HTTPS connection to a public URL. The signature check is what shows the provider sent the message.
Can I parse the JSON before verifying a Stripe signature?
No. Stripe needs the raw body exactly as sent. Parse it only after the signature passes.
Why does my Adyen signature fail after going live?
Adyen requires a new HMAC key for the live environment, and each key belongs to one endpoint.
Sources
- Stripe: Receive Stripe events in your webhook endpoint; Resolve webhook signature verification errors
- Adyen: Verify HMAC signatures; Secure webhooks; Handle webhook events
- PayPal: Integrate webhooks
Documentation reviewed October 5, 2026.




