Webhook signature verification for Razorpay, PayU and Cashfree
One page for all three schemes — which secret signs, which string is signed, and which encoding comes out. Every other comparison covers Stripe, GitHub and Shopify; these are the ones you actually integrate in India.
Every guide to verifying webhook signatures covers Stripe, GitHub, Slack and Shopify. If you are integrating payments in India you are working with Razorpay, PayU or Cashfree, and the three of them sign in three different ways — different secret, different string, different encoding. Porting one implementation to the next is the single most reliable way to produce a mismatch on a delivery that was never tampered with.
This is all three, side by side.
The short version
- Razorpay —
HMAC-SHA256over the raw body, hex, keyed with the webhook secret. - Cashfree —
HMAC-SHA256over timestamp + raw body, base64, keyed with the client secret. - PayU — plain
SHA-512over a pipe-joined field list that contains the salt. Not an HMAC at all.
Three properties differ between them, and each one alone changes the digest completely with no clue as to which: what gets signed, what key is used, and how the result is encoded.
Razorpay
The simplest of the three. The signed string is the request body exactly as it arrived — no timestamp, no prefix, nothing else concatenated.
hex( HMAC-SHA256( raw body, webhook secret ) ) header: X-Razorpay-Signaturebody {"entity":"event","account_id":"acc_BFQ7uQEaa7j2z7","event":"payment.captured","contains":["payment"],"created_at":1739510400}
secret whsec_razorpay_demo_secret
signature 397bb1d09334c5158b5bc4f63c5a9f3537970bad6755f50a6d2766dc483f348aThe trap here is not the algorithm, it is which secret. Razorpay has two entirely separate signing schemes, and the one people reach for first is the wrong one — see razorpay_signature vs webhook signature. Webhooks use the secret you typed when creating the webhook. The API key secret signs something else.
There is no timestamp in the signature, so replay protection is yours to add. De-duplicate on the X-Razorpay-Event-Id header.
Cashfree
Looks like Stripe, and is not. The timestamp arrives in its own header and is concatenated onto the front of the body with no separator — Stripe joins them with a dot, and that one character is a total mismatch.
base64( HMAC-SHA256( x-webhook-timestamp + raw body, client secret ) )
headers: x-webhook-signature, x-webhook-timestamptimestamp 1739510400
body {"type":"PAYMENT_SUCCESS_WEBHOOK","data":{"order":{"order_id":"order_demo_001","order_amount":499.00,"order_currency":"INR"}}}
secret cfsk_ma_demo_client_secret
signature FOnlgr0yZHNVBJOoMXiVu7OO7/wJ3YnNd6Rs/iWCaGs=Note the output is base64, not hex. A correct digest compared in the wrong encoding fails exactly like a wrong secret does. The full breakdown of the three ways this goes wrong is in why your Cashfree webhook signature does not match.
PayU
The outlier. PayU does not use an HMAC — it builds a pipe-delimited string, puts the merchant salt inside it, and takes a plain SHA-512 of the result. The hash also does not travel in a header: it is a hash field inside the posted form body.
hex( SHA-512( salt|status|||||udf5|udf4|udf3|udf2|udf1|email|firstname|productinfo|amount|txnid|key ) )Those five empty positions are udf6 through udf10, which PayU reserves and always sends blank. They must still be present. The field order is the request hash mirrored, with status inserted after the salt.
body key=gtKFFx&txnid=demo_txn_001&amount=499.00&productinfo=Pro%20plan&firstname=Asha&email=asha@example.com&udf1=&udf2=&udf3=&udf4=&udf5=&status=success
salt eCwWELxi
hash deb1f8406bedab59a44365988240aeeb220737b2fca2588754eb0516b1d76df7266fb6d246ed56a5bfb0a904322a72ca0e4634ceb6e8f2e665434ac812e11659Why one shared helper usually breaks
The natural instinct is a single verify(secret, body, signature) that switches on the gateway. It works for Razorpay and Cashfree and then fails for PayU, because the three differ in more than the key:
- What is signed — the raw body, a timestamp plus the body, or a list of named fields joined in a fixed order.
- What the key is — a webhook secret, a client secret, or a salt that is not a key at all but part of the message.
- Where the signature is — a header for two of them, a form field for PayU.
A helper that takes the signed string as an argument generalises. One that assumes the body is the signed string does not.
The rule that applies to all three
Checking a real one
If a delivery is failing and you cannot tell which of these details is wrong, paste it into the verifier. It reports which string it signed for the gateway you picked, which is almost always where the answer is — and it runs entirely in your browser, so the secret does not leave the machine.
If you do not have a delivery to hand, the free test endpoint gives you a URL to point the gateway at and shows you the raw bytes it received.
Check it against the tool
- Webhook signature verifier
Check a Razorpay, Stripe, Cashfree or PayU signature against your secret — webhooks, and Razorpay's checkout and payment-link schemes — and see exactly which string was signed.
- Free webhook testing endpoint
Get a URL that catches webhooks for fifteen minutes: see the exact bytes that arrived, check the signature against your secret in your browser, and forward every delivery to localhost with one command — no tunnel.
Frequently asked questions
- Which of the three is not an HMAC?
- PayU. Razorpay and Cashfree both key an HMAC-SHA256 with your secret, but PayU concatenates the salt into a pipe-delimited string and takes a plain SHA-512 of the whole thing. That means field order and empty fields matter enormously, and it means the salt appears inside the string being hashed rather than acting as a key.
- Can I use one verification function for all three?
- Only if it takes the signed string as an argument rather than assuming one. The algorithms differ in what is signed (raw body, timestamp plus body, or a pipe-joined field list), in the key (webhook secret, client secret, or merchant salt) and in the encoding (hex, base64, hex). A shared helper that only varies the secret will pass for one gateway and fail for the other two.
- Do any of them include a timestamp in the signature?
- Only Cashfree, which sends x-webhook-timestamp and concatenates it in front of the body with no separator. Razorpay signs the body alone, so replay protection is yours to add — de-duplicate on the X-Razorpay-Event-Id header. PayU's hash covers transaction fields rather than a time, so a captured response stays valid indefinitely.
- Which secret does each one use?
- Razorpay uses the webhook secret you chose when creating the webhook, not your API key secret. Cashfree uses the client secret for the environment the delivery came from, so a sandbox event checked against a production secret fails exactly like a tampered one. PayU uses the merchant salt matching the key in the payload, and the salt version PayU signed with.