</> Payments ToolboxWebhook capture

Why your PayU response hash does not match

Field order, empty udf slots and amount formatting all change the digest. The intermittent failures are usually additionalCharges silently switching PayU to a different formula.

PayU is the odd one out among Indian gateways, and the reason its hash mismatches are so hard to debug is that it is not an HMAC at all. It is a plain SHA-512 over a pipe-delimited string that has your salt concatenated into it, in an order that mirrors the request hash.

The reverse hash string
sha512( salt | status | | | | | | udf5 | udf4 | udf3 | udf2 | udf1 |
         email | firstname | productinfo | amount | txnid | key )

Every element of that line is a way to get it wrong: the order is reversed relative to the request hash, there are five mandatory empty fields in the middle, and status sits immediately after the salt.

The five empty fields

Between status and udf5 there are five empty strings. They are udf6 to udf10, which PayU reserves and always sends blank — and they must still be there as empty values between pipes. Omitting them shifts every field that follows and changes the digest entirely.

Here is a correct hash, and the same response with one udf field dropped:

Correct
deb1f8406bedab59a44365988240aeeb220737b2fca2588754eb0516b1d76df7266fb6d246ed56a5bfb0a904322a72ca0e4634ceb6e8f2e665434ac812e11659
With udf1 missing rather than empty
deb1f8406bedab59a44365988240aeeb220737b2fca2588754eb0516b1d76df7266fb6d246ed56a5bfb0a904322a72ca0e4634ceb6e8f2e665434ac812e11659

Amount formatting

The amount goes into the string exactly as PayU sends it. If your code normalises 499.00 to a number and back, you will produce a different string and a different hash:

amount as sent: 499.00
deb1f8406bedab59a44365988240aeeb220737b2fca2588754eb0516b1d76df7266fb6d246ed56a5bfb0a904322a72ca0e4634ceb6e8f2e665434ac812e11659
amount normalised to 499
2a215e80cfe77a1fe17b9438d04549d66c2dee8ca3a6a411052038f1b13a5604624555e07bf2b349d1ac2f76b42647f399b001578a12d389f5c4e68639b1c87a

Take the raw posted value. Do not parse it into a float, and do not reformat it — a trailing zero is part of the signed data.

The intermittent failure: additionalCharges

This is the one that costs whole afternoons, because it fails on some transactions and passes on others from the same endpoint with the same code.

When PayU includes an additionalCharges field, it prefixes that value to the front of the entire hash string and the generic formula stops applying:

With additionalCharges present
sha512( additionalCharges | salt | status | ... )

The same response, with and without the field, hashes to completely different values:

Without additionalCharges
deb1f8406bedab59a44365988240aeeb220737b2fca2588754eb0516b1d76df7266fb6d246ed56a5bfb0a904322a72ca0e4634ceb6e8f2e665434ac812e11659
With additionalCharges=25.00
363692ee1dbeaf8fbd593bbe8fb3ec6f9060b301a1276065b0e6cbc39dff63abf8a645f9e7b336016e57867720331052fc916d3a3f1d852e3974802802b54dfb

So a merchant who collects additional charges on some transactions is verifying against two different formulas without knowing it. Detect the field's presence — not its value — and switch formula accordingly.

Salt version

PayU supports multiple salt versions, and a response is signed with the version that was active when the transaction was created. If you rotate, transactions already in flight continue to arrive signed with the old salt. Keep the previous salt for at least as long as your longest settlement window, and try both before rejecting.

Finding which one you have

Paste the posted parameters and your salt into the webhook signature verifier and pick PayU. It shows the assembled hash string with the salt masked, detects the additionalCharges variant from the payload and says which formula it used, and takes the hash field straight from the body so you do not have to find it yourself. It runs entirely in your browser.

Comparing that string against the one your code builds — field by field, pipe by pipe — is how you find the difference in about thirty seconds instead of an afternoon. In almost every case it is a missing empty field or a reformatted amount.

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.

Frequently asked questions

Why is PayU's hash not an HMAC?
Because the salt is concatenated into the string being hashed rather than used as a key. It is a plain SHA-512 over salt|status|...|key. The practical consequence is that the hash string itself contains your salt, so it must never be logged or displayed — a debug print of the string you hashed is a disclosure of your merchant salt.
Why do only some transactions fail?
Almost always additionalCharges. When PayU includes that field it prefixes the value to the front of the hash string, and the generic formula no longer applies. A merchant who collects additional charges on some transactions and not others is verifying against two different formulas from one endpoint, which presents as an intermittent, unreproducible mismatch.
How many empty fields go between status and udf5?
Five. They are udf6 to udf10, which PayU reserves and always sends blank, and they must still be present as empty strings between the pipes. Omitting them shifts every field after them and changes the digest completely.