DEV Community

How to Test Razorpay Webhooks Locally

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 Razorpay Dashboard
  • Node.js 16+ and npm installed locally
  • Express.js or similar HTTP framework (we'll use Express in examples)
  • curl or Postman to trigger test requests
  • Understanding of HMAC-SHA256 signature verification (we'll cover it, but familiarity helps)

Setting Up Razorpay Webhook Testing Locally

Step 1: Create a Local Webhook Handler

Start by building a minimal Express server that accepts and logs Razorpay webhooks. This handler will verify the signature and process payment events.

const express = require('express');
const crypto = require('crypto');
const app = express();

// Middleware to capture raw body for signature verification
app.use(express.json({ verify: (req, res, buf) => {
    req.rawBody = buf.toString('utf8');
}}));

const RAZORPAY_KEY_SECRET = process.env.RAZORPAY_KEY_SECRET;

app.post('/webhooks/razorpay', (req, res) => {
    const signature = req.headers['x-razorpay-signature'];
    const body = req.rawBody;

    // Verify webhook signature
    const expectedSignature = crypto
        .createHmac('sha256', RAZORPAY_KEY_SECRET)
        .update(body)
        .digest('hex');

    if (signature !== expectedSignature) {
        console.error('โŒ Signature mismatch');
        return res.status(403).json({ error: 'Invalid signature' });
    }

    console.log('โœ… Signature verified');
    console.log('Event:', req.body.event);
    console.log('Payload:', JSON.stringify(req.body, null, 2));

    // Handle specific events
    switch (req.body.event) {
        case 'payment.authorized':
            console.log('Payment authorized:', req.body.payload.payment.entity.id);
            break;
        case 'payment.failed':
            console.log('Payment failed:', req.body.payload.payment.entity.id);
            break;
        case 'payment.captured':
            console.log('Payment captured:', req.body.payload.payment.entity.id);
            break;
        default:
            console.log('Unhandled event:', req.body.event);
    }

    // Acknowledge receipt immediately
    res.status(200).json({ status: 'ok' });
});

app.listen(3000, () => {
    console.log('Webhook server running on http://localhost:3000');
});

Save this as webhook-server.js and start it:

export RAZORPAY_KEY_SECRET="your_key_secret_here"
node webhook-server.js

Step 2: Expose Your Localhost to Razorpay Using a Stable Tunnel

Your local server is not accessible from the internet. You need a tunnel that forwards Razorpay's requests to localhost. Unlike temporary tools that break on restart, use a stable named endpoint:

npx @anonymilyhq/cli listen 3000

This command:

  • Captures webhooks in the cloud at a stable HTTPS endpoint like https://api.anonymily.com/h/your-endpoint-name
  • Forwards them 1:1 to http://localhost:3000 over Server-Sent Events
  • Survives restarts and redeploys (the endpoint URL stays the same)
  • Works even if localhost is temporarily down (requests queue in the cloud)

The output shows your public endpoint URL. Copy it.

Step 3: Register the Webhook Endpoint in Razorpay Dashboard

  • Log in to Razorpay Dashboard
  • 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.authorized, payment.failed, payment.captured
  • Click Create Webhook

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:

  • You're hashing the parsed JSON instead of the raw request body
  • Your RAZORPAY_KEY_SECRET is wrong or truncated
  • The request body was modified before hashing

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:

  • Verify the handler is running: curl http://localhost:3000/webhooks/razorpay should not return "Connection refused"
  • Check the port matches your tunnel command: npx @anonymilyhq/cli listen 3000 must match your Express app.listen(3000)
  • Check for startup errors in your handler logs (e.g., missing RAZORPAY_KEY_SECRET env var)

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 Anonymily Pro. If you're also testing webhooks from other providers, see Test Webhooks Locally Without ngrok for a broader strategy. For deep dives into signature verification across providers, read Mastering Webhook Signature Verification.

Comments

No comments yet. Start the discussion.