Webhook Setup
Configuring Razorpay webhooks to handle payment events reliably — even when the customer's internet drops mid-payment.
Webhooks solve a problem that the payment flow alone cannot: what happens if the customer's internet disconnects after payment but before your app can verify it?
The payment went through. The money left the parent's account. But your verify-payment edge function never got the success response from the modal, so the fee record is still showing as "pending."
The parent calls you. You look at your database. It shows pending. You look at Razorpay. It shows paid. This mismatch is a support nightmare.
Webhooks eliminate this mismatch. Razorpay sends an HTTP POST to your edge function for every significant payment event, from Razorpay's servers — independent of whether the customer's browser is open or closed.
The "Why First" Scenario — In CA Terms
Think of it like NEFT confirmations. When a client transfers money to your firm's account, two things happen:
- The client's net banking app shows "Transfer successful" — that's like the frontend handler
- Your bank sends you an SMS confirmation + it appears in your bank statement — that's the webhook
Your accounts team doesn't rely only on the client telling you "I've paid." They verify against the bank statement. The webhook is your bank statement for every payment.
What Events to Subscribe To
Razorpay offers webhooks for many events. For a school fee application, subscribe to these:
| Event | When it fires | Why you need it |
|---|---|---|
payment.captured | Payment is successful and money is captured | Mark fee as paid when frontend verification fails |
payment.failed | Parent's payment attempt failed | Update fee record to show failure, allow retry |
order.paid | The order's total amount has been fully paid | Useful for partial payment scenarios (not needed in basic flow) |
refund.created | You issue a refund via Razorpay dashboard | Update fee record status to refunded |
refund.processed | Razorpay confirms refund reached parent | Update parent-facing status |
For EduTrack in training: start with payment.captured and payment.failed. Add refund events when you build the cancellation/refund flow.
Setting Up the Webhook in Razorpay Dashboard
Deploy the webhook edge function first
The webhook URL must be a live HTTPS endpoint before you can register it in Razorpay — Razorpay validates the URL during setup. Deploy your edge function to Supabase first.
Your webhook URL format: https://[project-ref].supabase.co/functions/v1/razorpay-webhook
Find your project ref in the Supabase dashboard URL or by running the Supabase CLI status command.
Create the webhook secret
Generate a strong random string. This is separate from your Razorpay API keys — it is used exclusively to verify that incoming webhook requests are genuinely from Razorpay.
Save it to VaultMate immediately:
| Field | Value |
|---|---|
| Title | Razorpay Webhook Secret |
| Secret | (the random string you generated) |
| Notes | Used to verify webhook signatures in razorpay-webhook edge function |
| Category | API Key |
Then set it as a Supabase secret using the Supabase CLI.
Register the webhook in Razorpay
Settings → Webhooks. Add the Supabase Edge Function URL as the endpoint — Razorpay will POST every subscribed event here.
Go to Razorpay Dashboard → Settings → Webhooks → Create New Webhook. Fill in the form:
| Field | What to enter |
|---|---|
| Webhook URL | https://[project-ref].supabase.co/functions/v1/razorpay-webhook |
| Secret | The random string you generated in the previous step |
| Alert Email | Your project email (Razorpay emails this if the webhook starts failing) |
| Active Events | Check payment.captured and payment.failed |
Click Create Webhook. Razorpay will attempt to send a test event. If your edge function is deployed and returns a 200, the webhook shows as "Active."
The Webhook Edge Function
Always return 200 to Razorpay, even for events you do not handle. If your function returns a non-200 response, Razorpay treats it as a delivery failure and retries the webhook. Multiple retries for the same event can cause duplicate processing (marking a fee as paid twice, for example). Return 200 to acknowledge receipt, then decide internally whether to act on the event.
Idempotency — Processing Each Event Exactly Once
Webhooks can be delivered more than once. Network issues, Razorpay retries, and edge function restarts can all cause the same event to arrive multiple times. Your webhook handler must be idempotent — processing the same event a second time must produce the same result as the first.
The pattern in the code above: check before updating.
This is exactly how bank reconciliation works: before posting a credit entry, your team checks whether it was already posted. Duplicate entries are caught before they create errors, not after.
Testing Webhooks During Development
Razorpay cannot reach your local development machine (it is not on the public internet). Options:
Option 1: Use Razorpay's Test Mode webhook trigger
In the Razorpay Dashboard → Settings → Webhooks, there is a "Test" button next to each registered webhook. It sends a sample payload to your registered URL. This only works if you have deployed the edge function.
Option 2: Deploy to Supabase and test there
During development, you can deploy your edge function to Supabase's hosted environment and register that URL with Razorpay's Test Mode webhook. Run test payments — Razorpay delivers the webhook to your deployed function, which updates the test database.
Option 3: Use a tool like ngrok (advanced)
ngrok creates a temporary public URL that tunnels to your local machine. Beyond this training's scope — Option 2 is simpler for this stack.
Monitoring Webhook Delivery
Razorpay Dashboard → Settings → Webhooks → click your webhook → View Attempts
For every webhook event, you can see:
- Timestamp of delivery attempt
- HTTP response code your function returned
- Number of retries if it failed
- The full payload that was sent
This is invaluable for debugging. If a fee record is stuck in "pending" and the Razorpay dashboard shows the payment as captured, check webhook attempts — your function likely returned a non-200 error.
Razorpay's Retry Policy
If your function returns anything other than 200, Razorpay retries according to this schedule:
| Attempt | Delay after previous attempt |
|---|---|
| 1st retry | 15 minutes |
| 2nd retry | 30 minutes |
| 3rd retry | 1 hour |
| 4th retry | 6 hours |
| 5th retry | 24 hours |
After 5 failed retries, Razorpay marks the webhook as failed and sends an email to your alert address. You can manually trigger a re-delivery from the Attempts view.
The implication: if your edge function has a bug and is returning 500, you have approximately 32 hours before Razorpay gives up. Fix the function, then manually re-trigger the failed webhooks.