Razorpay is one of the most common payment gateways we wire into .NET Core apps for clients doing business in India, and the integration pattern is the same regardless of the product: create an order on the server, let Razorpay Checkout collect payment details on the client, then — this is the part people skip and regret — verify the payment signature on the server before you ever mark an order as paid.
Creating an Order Server-Side
Every Razorpay payment starts with an order created through the Orders API, using your key ID and key secret from the Razorpay dashboard. The order amount is always in the smallest currency unit — paise for INR, so 500 rupees is 50000. Use the official Razorpay.NET package rather than hand-rolling the HTTP calls.
using Razorpay.Api;
public class RazorpayService
{
private readonly RazorpayClient _client;
public RazorpayService(IConfiguration config)
{
_client = new RazorpayClient(
config["Razorpay:KeyId"],
config["Razorpay:KeySecret"]);
}
public Order CreateOrder(decimal amountInRupees, string receiptId)
{
var options = new Dictionary<string, object>
{
{ "amount", (int)(amountInRupees * 100) },
{ "currency", "INR" },
{ "receipt", receiptId },
{ "payment_capture", 1 }
};
return _client.Order.Create(options);
}
}
payment_capture set to 1 tells Razorpay to auto-capture the payment as soon as authorization succeeds, rather than leaving it authorized and requiring a separate capture call — the right default for most checkout flows. Return the order id from this endpoint to the client; that is the only thing the frontend needs to open Checkout.
Opening Checkout on the Client
The frontend loads Razorpay's checkout.js and passes the order_id you just created along with the public key id (never the key secret).
const options = {
key: "rzp_test_yourKeyId",
order_id: serverOrder.id,
amount: serverOrder.amount,
currency: "INR",
name: "Xtreme Thoughts Softtech",
handler: function (response) {
fetch("/api/payments/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
razorpay_order_id: response.razorpay_order_id,
razorpay_payment_id: response.razorpay_payment_id,
razorpay_signature: response.razorpay_signature
})
});
}
};
const rzp = new Razorpay(options);
rzp.open();
Verifying the Signature Server-Side
The handler callback firing does not mean the payment is trustworthy — it runs entirely in the browser and a response object is trivial to fake from dev tools. The only proof that Razorpay actually processed this payment is the signature, an HMAC SHA256 hash of the order id and payment id joined by a pipe, computed using your key secret. Recompute it yourself and compare before touching your order status.
using System.Security.Cryptography;
using System.Text;
public bool VerifySignature(
string orderId, string paymentId, string signature, string keySecret)
{
var payload = $"{orderId}|{paymentId}";
var keyBytes = Encoding.UTF8.GetBytes(keySecret);
var payloadBytes = Encoding.UTF8.GetBytes(payload);
using var hmac = new HMACSHA256(keyBytes);
var hash = hmac.ComputeHash(payloadBytes);
var generatedSignature = Convert.ToHexString(hash).ToLowerInvariant();
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(generatedSignature),
Encoding.UTF8.GetBytes(signature));
}
Only mark the order as paid in your database after this returns true. Use FixedTimeEquals rather than a plain string comparison — a naive equality comparison leaks timing information about how many leading characters matched, which is a real (if obscure) side channel for anyone trying to forge a signature.
Handling Webhooks for Async Confirmation
The client-side flow can fail to report back — a closed tab, a network drop right after payment succeeds — leaving your order stuck as unpaid even though Razorpay actually captured the money. Configure a webhook in the Razorpay dashboard for the payment.captured event so your server hears about it independently. Webhook payloads carry their own signature in the X-Razorpay-Signature header, verified the same HMAC way but against the raw request body and your webhook secret (not your key secret).
- Always verify the payment signature server-side — never trust the client-side handler callback alone.
- Amounts are always in the smallest currency unit (paise for INR) on both order creation and the amount Checkout displays.
- Configure the payment.captured and payment.failed webhooks so orders reconcile even when the browser flow is interrupted.
- Store the Razorpay order id, payment id, and signature against your own order record for auditing and dispute handling.
- Use test mode keys throughout development — Razorpay provides test card numbers that simulate both success and failure.
Make webhook handling idempotent. Razorpay can and does retry a webhook delivery if your endpoint does not respond with a 200 quickly enough, so check whether you have already processed that payment id before applying the update a second time.
Wrapping Up
The parts that actually matter here are the two things easy to skip under deadline pressure: verifying the signature before trusting a payment, and having a webhook as a backstop for whenever the client-side flow does not complete cleanly. Get those two right and the rest of the integration is mostly plumbing.