Server-side orders, HMAC-verified webhooks, idempotent fulfilment and refunds — for Next.js 14 (Pages Router). Built for the India-first stack, not another Stripe tutorial with the names swapped.
Buy now — ₹1500UPI · Cards · Netbanking · One-time purchase. Instant download. Free updates. Test-mode ready.
I wrote this kit after reading through several. The same three mistakes showed up almost every time.
The browser POSTs { amount: 500 } and the server trusts it. Anyone opens devtools, changes it to 1, and buys your ₹5000 product for ₹1. Here the client sends only a product id — the server looks up the real amount.
An unverified webhook lets anyone who guesses the URL POST a fake payment.captured and get free product. Every request here is checked against x-razorpay-signature over the raw body before a byte of payload is trusted.
Razorpay retries. Without an atomic guard you double-fulfil — you ship twice and get paid once. Fulfilment here goes through a check-and-set that returns true for exactly one delivery per order.
Webhook signature verification needs the request's raw bytes. Re-serialising JSON changes key ordering and whitespace, so the signature can never match — and the only error you get is "invalid signature", with no hint why.
// The body parser must be OFF — the signature covers the raw bytes.
// If you `JSON.parse` first and re-serialise, this never matches.
export const config = { api: { bodyParser: false } };
const rawBody = await readRawBody(req); // raw stream
const valid = verifyWebhookSignature(
rawBody,
req.headers["x-razorpay-signature"] // timing-safe HMAC
);
if (!valid) return res.status(400).json({ error: "Invalid signature." });
// Only NOW is the payload trustworthy.
const event = JSON.parse(rawBody.toString("utf8"));
// Atomic: true for exactly ONE delivery per order.
const { claimed } = await orderStore.claimForFulfilment(orderId, paymentId);
if (!claimed) return res.status(200).json({ duplicate: true }); // not 500!
Before release I drove the real webhook handler with mock requests. These are actual results, not marketing claims.
| Test | Result |
|---|---|
| Valid checkout signature accepted | PASS |
| Signature for a different order or payment rejected | PASS |
| Signature computed with the wrong secret rejected | PASS |
| Tampered, unsigned and reordered-body webhooks rejected | PASS |
| Duplicate deliveries do not re-fulfil | PASS |
| 5 concurrent deliveries → exactly 1 fulfilment | PASS |
| Late payment.failed cannot downgrade a paid order | PASS |
| Duplicate refund events are a no-op | PASS |
| TypeScript compiles under strict: true | PASS |
49 assertions, all passing. 17 on signature verification, 32 on the webhook handler.
One-time. No subscription. Free updates to whatever you buy.
Everything included. No tiers, no upsell.
Payment: secure Razorpay checkout — UPI, cards, netbanking and wallets. Your download is emailed to the address you provide at checkout.
Want to see the code first? The core of it is free and MIT licensed on GitHub — browse the repo.
Pages Router, not App Router. This is built on pages/ and pages/api/ for Next.js 14. There is no App Router version yet — if you specifically need app/, don't buy this.
Pages Router. It's built on pages/ and pages/api/. There is no App Router version.
No. It ships with a JSON order store behind a six-method interface. For production multi-instance hosting, swap it for Postgres by editing one file.
Yes. 49 assertions run against the real webhook and signature handlers — 17 on signature verification, 32 on the webhook handler. TypeScript compiles clean under strict: true.
Full and partial refunds are supported through an authenticated endpoint that checks the refundable balance first.
Licensed for one commercial project. If you need it for multiple client projects, email support@nethercore.in and we'll sort something out.
No. Razorpay test mode works without business verification. You only need a verified account to accept live payments.