Dev.to · 2 min read

Verifying Razorpay subscription webhooks in ASP.NET Core (the parts that bit me)

Verifying Razorpay subscription webhooks in ASP.NET Core (the parts that bit me)

Razorpay's subscription webhooks look simple: receive JSON, update the plan. In practice three things break first — signature checks, duplicate deliveries, and testing locally. Here's how I handle each in .NET 10. Verify against the raw body, with the webhook secret The signature in X-Razorpay-Signature is an HMAC-SHA256 of the raw request body. If you let model binding parse it first and re-serialize, the bytes change and verification fails. Also: the webhook secret is the one you set in the dashboard webhook settings — not your API key secret. Mixing them up is the most common mistake. csharp app.MapPost("/api/billing/webhook", async (HttpRequest req, IOptions opt, BillingService billing) => { using var reader = new StreamReader(req.Body); var body = await reader.ReadToEndAsync(); var expected = Convert.ToHexString( HMACSHA256.HashData(Encoding.UTF8.GetBytes(opt.Value.WebhookSecret), Encoding.UTF8.GetBytes(body))) .ToLowerInvariant(); var actual = req.Headers["X-Razorpay-Signature"].ToString(); if (!CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(actual))) return Results.Unauthorized(); await billing.HandleAsync(req.Headers["x-razorpay-event-id"].ToString(), body); return Results.Ok(); }); FixedTimeEquals avoids leaking timing information on comparison. Assume every event arrives more than once Razorpay retries when you don't return 2xx quickly, so the same subscription.charged can land twice. Store the event ID with a unique index and skip anything already seen. Do the insert and the state change in one transaction. Handle the lifecycle, not just "paid" The events that matter: subscription.activated subscription.charged subscription.pending — payment failed, Razorpay is retrying subscription.halted — retries exhausted subscription.cancelled Map them to your own status enum and gate features on your status — never call Razorpay on every request. Test with real webhooks locally Use test mode plus a tunnel (cloudflared or ngrok) pointed at your local API. Mocks won't catch the raw-body or secret mistakes above. I packaged this (plus multi-tenancy, auth and a React frontend) into a .NET 10 SaaS starter — early access here. Questions about the webhook flow? Ask in the comments.

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More Programming & Dev News