<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ranveer Singh</title>
    <description>The latest articles on DEV Community by Ranveer Singh (@ranveersidhu).</description>
    <link>https://dev.to/ranveersidhu</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4207781%2F93bfe7f8-47e5-4c49-9b9c-24619c775436.jpg</url>
      <title>DEV Community: Ranveer Singh</title>
      <link>https://dev.to/ranveersidhu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ranveersidhu"/>
    <language>en</language>
    <item>
      <title>Verifying Razorpay subscription webhooks in ASP.NET Core (the parts that bit me)</title>
      <dc:creator>Ranveer Singh</dc:creator>
      <pubDate>Mon, 26 Oct 2026 05:38:05 +0000</pubDate>
      <link>https://dev.to/ranveersidhu/verifying-razorpay-subscription-webhooks-in-aspnet-core-the-parts-that-bit-me-1el3</link>
      <guid>https://dev.to/ranveersidhu/verifying-razorpay-subscription-webhooks-in-aspnet-core-the-parts-that-bit-me-1el3</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Verify against the raw body, with the webhook secret&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
app.MapPost("/api/billing/webhook", async (HttpRequest req, IOptions opt, BillingService billing) =&amp;gt;&lt;br&gt;
{&lt;br&gt;
    using var reader = new StreamReader(req.Body);&lt;br&gt;
    var body = await reader.ReadToEndAsync();&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;});&lt;/p&gt;

&lt;p&gt;FixedTimeEquals avoids leaking timing information on comparison.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Assume every event arrives more than once&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Handle the lifecycle, not just "paid"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The events that matter:&lt;/p&gt;

&lt;p&gt;subscription.activated&lt;br&gt;
subscription.charged&lt;br&gt;
subscription.pending — payment failed, Razorpay is retrying&lt;br&gt;
subscription.halted — retries exhausted&lt;br&gt;
subscription.cancelled&lt;/p&gt;

&lt;p&gt;Map them to your own status enum and gate features on your status — never call Razorpay on every request.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Test with real webhooks locally&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;I packaged this (plus multi-tenancy, auth and a React frontend) into a .NET 10 SaaS starter — early access here.&lt;/p&gt;

&lt;p&gt;Questions about the webhook flow? Ask in the comments.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>webdev</category>
      <category>payments</category>
    </item>
  </channel>
</rss>
