The Challenge: Testing Razorpay Webhooks Without Public Infrastructure
Testing Razorpay payment webhooks locally is frustrating. Razorpay's webhook system requires a publicly accessible HTTPS endpoint to deliver payment events—but your localhost isn't reachable from the internet. You're forced to either deploy to staging for every test, use temporary tunnels that break on restart, or skip webhook testing altogether until production. None of these options catch bugs early or let you debug signature verification, payload parsing, or idempotency logic safely.
When you test Razorpay webhooks locally, you need three things: a stable endpoint URL that Razorpay can reach, the ability to replay events without re-triggering payments, and local access to the raw webhook payload for debugging. This guide shows you how to set up all three.
Prerequisites
Razorpay account with API keys (Key ID and Key Secret) from the
- Navigate to Settings → Webhooks
- Click Add New Webhook
- Paste your Anonymily endpoint URL (e.g.,
https://api.anonymily.com/h/your-endpoint-name) into the URL field - Select events to subscribe to:
payment.authorizedpayment.failedpayment.captured
- Click Create Webhook
- You're hashing the parsed JSON instead of the raw request body
- Your
RAZORPAY_KEY_SECRETis wrong or truncated - The request body was modified before hashing
- Verify the handler is running:
curl http://localhost:3000/webhooks/razorpayshould not return "Connection refused" - Check the port matches your tunnel command:
npx @anonymilyhq/cli listen 3000must match your Expressapp.listen(3000)
- Check for startup errors in your handler logs (e.g., missing
RAZORPAY_KEY_SECRETenv var)
Razorpay will send a test event. If your handler is running and the tunnel is active, you'll see the signature verification succeed and the event logged locally.
Step 4: Trigger a Test Payment and Observe the Webhook
Create a test payment using Razorpay's test mode (use card 4111 1111 1111 1111 with any future expiry and CVV). When the payment completes, Razorpay sends a webhook to your registered endpoint. The Anonymily tunnel captures it and forwards it to your local handler.
You should see output like:
✅ Signature verified
Event: payment.captured
Payload: {
"event": "payment.captured",
"payload": {
"payment": {
"entity": {
"id": "pay_1234567890abcd",
"amount": 50000,
"currency": "INR",
"status": "captured"
}
}
}
}
Common Errors and Fixes
Error 1: ❌ Signature mismatch
Root Cause:
The x-razorpay-signature header doesn't match your computed HMAC. This usually happens because:
Fix:
Always capture the raw body before JSON parsing. Express's default express.json() discards the raw bytes. Use the verify callback:
app.use(express.json({ verify: (req, res, buf) => {
req.rawBody = buf.toString('utf8');
}}));
Then hash req.rawBody, not JSON.stringify(req.body).
Error 2: ECONNREFUSED: Connection refused (localhost:3000)
Root Cause:
Your webhook handler crashed, wasn't started, or is listening on a different port. The tunnel tries to forward the webhook but can't connect.
Fix:
Frequently Asked Questions
Q: Can I replay a webhook without re-triggering a payment?
A: Yes. After a webhook is captured, you can replay it to test error handling or idempotency logic. With the free tier, you get one replay. Pro tier ($9/month) adds modify-and-replay: change the payload, re-sign it with your key, and replay—all within the Anonymily dashboard. This is invaluable for testing edge cases like duplicate payment IDs or refund scenarios.
Q: How do I know if my signature verification is correct?
A: The honest way: capture a real webhook from Razorpay, log both the header signature and your computed signature, and compare them. If they match, your logic is correct. Pro tier includes a signature verification helper that validates your implementation against provider-signed synthetic events—Razorpay included—without needing to trigger real payments.
Q: What's the difference between testing locally and in production?
A: Locally, you control the environment and can replay events infinitely. In production, webhooks arrive once, in order, with no replay. Test idempotency (handling the same event twice), error recovery (what if your handler times out?), and signature verification locally. Production is for monitoring and alerting when things go wrong.
Next Steps
Set up your local Razorpay webhook handler now:
npx @anonymilyhq/cli listen 3000
Your stable endpoint URL appears in the output. Register it in Razorpay Dashboard, trigger a test payment, and watch the webhook flow into your local logs. For replay, signature verification helpers, and provider-signed synthetic events, explore for a broader strategy. For deep dives into signature verification across providers, read Mastering Webhook Signature Verification.
SOCIAL SHARE CARD GENERATOR