## TL;DR

Recompute the HMAC-SHA256 of the raw request body bytes using your Razorpay webhook secret, and compare it to the header with a constant-time comparison. Mismatches almost always come from signing the parsed JSON instead of the raw body, or from using the wrong secret (test versus live). Sign the raw bytes with the right secret.

```text
razorpay webhook error: X-Razorpay-Signature mismatch on verification
```

## Steps

1. Capture the raw request body exactly as it arrived, before any JSON parsing. Expected: you have the untouched byte string the signature was computed over.

2. Confirm which webhook secret your code uses, and whether the event came from test mode or live mode. Expected: the secret matches the mode of the incoming event.

3. Compute the HMAC-SHA256 digest of the raw body with that secret, in hex form. Expected: you have a digest you can compare against the header value.

4. Compare using a constant-time comparison function, not a plain equality check. Expected: the comparison is timing-safe and accurate.

5. Re-run verification on a fresh delivery. Expected: the computed signature matches the header and the event is accepted.

## Use this when

- Verification fails with exactly "X-Razorpay-Signature mismatch".
- Webhooks verify in test mode but fail in live mode (or the reverse).
- A generated handler parses the body to JSON and re-serializes before signing.

## Not for this skill when

- Webhook events never arrive (that is a delivery or endpoint registration problem).
- Razorpay API calls fail authentication (that is your API key, not the webhook secret).
- The signature header belongs to a different provider with its own scheme.

## Variant phrasings

- Razorpay webhook signature verification failed
- Razorpay X-Razorpay-Signature does not match
- Razorpay webhook secret signature invalid

## Why it happens

Razorpay signs the exact bytes it sent. Any change to those bytes breaks the signature: parsing the JSON and re-serializing it alters whitespace and key order, middleware that reads the body stream can leave the handler with an empty string, and mixing up the test and live webhook secrets signs with the wrong key. The digest then never matches the header.

## Edge cases

- Framework body parsers often consume the raw stream; read the raw body in middleware before the JSON parser runs.
- The webhook secret is not your API key; they live in different dashboard sections and are not interchangeable.
- Retried deliveries carry the same signature; do not treat a duplicate delivery as a signature problem.

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_q1QhZvnzTqi2RXH77lYlAQ
