DevKit Market
  • Home
  • Categories
  • Products
  • Tools
  • Claude skills
  • Blog
  • About
Sign inGet started
DevKit Market
HomeCategoriesProductsToolsClaude skillsBlogAbout
Theme
Sign inGet started
DevKit Market

Production-ready starter kits for developers who want to ship faster, not fiddle with boilerplate.

Products

SaaS Starter ProNext.js Blog KitAuth BoilerplateLanding Page KitAdmin DashboardWaitlist App

Company

Hire meBlogClaude skillsAbout

Support

FAQContact

© 2026 DevKit Market. Built solo with Next.js & Claude.

Sitemap
Blog/Tutorial/How to Add Razorpay to Next.js (2026): Complete Guide with Code
Tutorial
April 12, 2026•8 min read

How to Add Razorpay to Next.js (2026): Complete Guide with Code

Nikhil Anand
Lead Developer @ DevKit
Adding Razorpay to a Next.js app used to be a half-day job. With Next.js 15, the App Router, and the new Server Actions pattern, you can ship a working checkout in about an hour — if you know what to skip and what not to skip.
This guide walks you through the full integration: order creation, the checkout popup, signature verification, webhooks, refunds, and the boring-but-critical "going live" checklist that breaks 80% of first-time integrations.
Everything here is written for Next.js 15 (App Router) + TypeScript in April 2026. I've built this exact integration five times for client projects, so the gotchas are real ones, not theoretical.
Live demo: your-demo-url.vercel.app GitHub repo: github.com/your-handle/razorpay-nextjs-2026

TL;DR — The 7-Step Version

  1. Create a Razorpay account, switch to test mode, copy your
    text
    key_id
    and
    text
    key_secret
    .
  2. Store them in
    text
    .env.local
    . Never commit them.
  3. Build a
    text
    /api/order
    route that creates an order on Razorpay's server.
  4. Load the Razorpay checkout script on your client component.
  5. Open the checkout popup with the
    text
    order_id
    you got back.
  6. Verify the returned signature on your server using HMAC SHA256.
  7. Set up a webhook for the cases where the user closes the browser before redirect.
If you only do steps 1-6, you'll lose ~3% of your payments to network drops. Step 7 is what separates a demo from production.

What You'll Build

A pricing page with a "Pay ₹499" button. Clicking it opens the Razorpay checkout, the user pays with UPI/card/netbanking, and on success you store the payment in your database and redirect them to a thank-you page.
The same pattern works for SaaS subscriptions, course purchases, donations, and one-time products.

Prerequisites

Before you start, make sure you have:
  • Node.js 20 or later (Razorpay's SDK uses native fetch)
  • Next.js 15.x project with the App Router
  • A Razorpay account (free, no KYC needed for test mode)
  • Basic familiarity with TypeScript and async/await
If you don't have a Next.js project yet, spin one up:
bash
npx create-next-app@latest razorpay-demo --typescript --app --tailwind
cd razorpay-demo

Step 1 — Create a Razorpay Account and Get Your API Keys

Go to dashboard.razorpay.com and sign up. You don't need to complete KYC to use test mode — that's only required when you flip to live mode.
Once you're in:
  1. Make sure the toggle in the top-right says Test Mode (not Live).
  2. Go to Account & Settings → API Keys.
  3. Click Generate Test Key.
  4. Copy both the Key ID (starts with
    text
    rzp_test_
    ) and the Key Secret.
The secret is shown only once. If you lose it, you'll have to regenerate the pair.

Step 2 — Install the Razorpay SDK

In your Next.js project root:
bash
npm install razorpay
npm install --save-dev @types/razorpay
That's it. You don't need axios, you don't need any wrapper library. The Razorpay Node SDK is the only server-side dependency you actually need.

Step 3 — Configure Environment Variables (the Secure Way)

Create a
text
.env.local
file in your project root:
bash
# .env.local
RAZORPAY_KEY_ID=rzp_test_YourKeyHere
RAZORPAY_KEY_SECRET=YourSecretHere
NEXT_PUBLIC_RAZORPAY_KEY_ID=rzp_test_YourKeyHere
RAZORPAY_WEBHOOK_SECRET=AnyStrongRandomString
A few things most tutorials get wrong here:
  • Only the
    text
    key_id
    is exposed to the client.
    That's why it has the
    text
    NEXT_PUBLIC_
    prefix. The secret stays server-only.
  • Do not put the secret behind
    text
    NEXT_PUBLIC_
    . I've seen this in production code on GitHub. It leaks your secret to every visitor.
  • Add
    text
    .env.local
    to
    text
    .gitignore
    (Next.js does this by default, but double-check).
  • The webhook secret is something you make up — we'll wire it into Razorpay's dashboard in Step 7.

Step 4 — Create the Order API Route

Razorpay's flow is: your server creates an order → client opens checkout with that order ID → user pays → you verify the signature. You never let the client decide the amount, because the client can be tampered with.
Create
text
app/api/order/route.ts
:
typescript
import { NextRequest, NextResponse } from "next/server";
import Razorpay from "razorpay";

const razorpay = new Razorpay({
  key_id: process.env.RAZORPAY_KEY_ID!,
  key_secret: process.env.RAZORPAY_KEY_SECRET!,
});

export async function POST(req: NextRequest) {
  try {
    const { amount, currency = "INR", receipt } = await req.json();

    if (!amount || amount < 100) {
      return NextResponse.json(
        { error: "Amount must be at least 100 paise (₹1)" },
        { status: 400 }
      );
    }

    const order = await razorpay.orders.create({
      amount, // in paise — ₹499 = 49900
      currency,
      receipt: receipt || `rcpt_${Date.now()}`,
    });

    return NextResponse.json({ orderId: order.id, amount: order.amount });
  } catch (err) {
    console.error("Razorpay order creation failed:", err);
    return NextResponse.json(
      { error: "Could not create order" },
      { status: 500 }
    );
  }
}
The two things to remember: amounts are always in paise (₹1 = 100), and always validate the amount on the server even if your UI sends it. A user could open DevTools and change ₹499 to ₹1.

Optional: The Server Actions Approach (Next.js 15)

If you prefer Server Actions over API routes, you can do this:
typescript
// app/actions/payment.ts
"use server";

import Razorpay from "razorpay";

const razorpay = new Razorpay({
  key_id: process.env.RAZORPAY_KEY_ID!,
  key_secret: process.env.RAZORPAY_KEY_SECRET!,
});

export async function createOrder(amount: number) {
  if (!amount || amount < 100) throw new Error("Invalid amount");

  const order = await razorpay.orders.create({
    amount,
    currency: "INR",
    receipt: `rcpt_${Date.now()}`,
  });

  return { orderId: order.id, amount: order.amount };
}
Then call it from your component with
text
await createOrder(49900)
instead of fetching
text
/api/order
. Cleaner, type-safe end-to-end, and one less roundtrip to manage.

Step 5 — Build the Checkout Component

Now the client side. Create
text
app/checkout/page.tsx
:
typescript
"use client";

import { useState } from "react";
import Script from "next/script";

declare global {
  interface Window {
    Razorpay: any;
  }
}

export default function CheckoutPage() {
  const [loading, setLoading] = useState(false);

  const handlePayment = async () => {
    setLoading(true);

    // 1. Create the order on the server
    const res = await fetch("/api/order", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ amount: 49900 }), // ₹499
    });

    const { orderId } = await res.json();

    // 2. Open the Razorpay checkout
    const options = {
      key: process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID,
      amount: 49900,
      currency: "INR",
      name: "Your Store Name",
      description: "Pro Plan — Monthly",
      order_id: orderId,
      handler: async (response: any) => {
        // 3. Verify on the server
        const verify = await fetch("/api/verify", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(response),
        });

        const result = await verify.json();
        if (result.success) {
          window.location.href = "/thank-you";
        } else {
          alert("Payment verification failed. Contact support.");
        }
      },
      prefill: {
        name: "Test User",
        email: "test@example.com",
        contact: "9999999999",
      },
      theme: { color: "#0f172a" },
    };

    const rzp = new window.Razorpay(options);
    rzp.open();
    setLoading(false);
  };

  return (
    <>
      &lt;Script src="https://checkout.razorpay.com/v1/checkout.js" />
      <main className="flex min-h-screen items-center justify-center">
        <button
          onClick={handlePayment}
          disabled={loading}
          className="rounded-lg bg-slate-900 px-8 py-3 text-white"
        >
          {loading ? "Loading..." : "Pay ₹499"}
        </button>
      </main>
    </>
  );
}
Three details that trip people up:
  • Use Next.js
    text
    &lt;Script>
    , not a raw
    text
    &lt;script>
    tag. Next.js handles loading order properly.
  • The
    text
    handler
    runs on success.
    It receives
    text
    razorpay_payment_id
    ,
    text
    razorpay_order_id
    , and
    text
    razorpay_signature
    . You send all three to your verify endpoint.
  • Don't trust the handler alone. If the user closes their browser between paying and the handler firing, you'll think the payment failed when it actually succeeded. That's what webhooks (Step 7) are for.

Step 6 — Verify the Payment Signature

This is the security-critical step. Razorpay signs every successful payment with HMAC SHA256 using your secret. If the signature doesn't match, someone is faking a payment.
Create
text
app/api/verify/route.ts
:
typescript
import { NextRequest, NextResponse } from "next/server";
import crypto from "crypto";

export async function POST(req: NextRequest) {
  const { razorpay_order_id, razorpay_payment_id, razorpay_signature } =
    await req.json();

  const body = `${razorpay_order_id}|${razorpay_payment_id}`;

  const expectedSignature = crypto
    .createHmac("sha256", process.env.RAZORPAY_KEY_SECRET!)
    .update(body)
    .digest("hex");

  const isAuthentic = expectedSignature === razorpay_signature;

  if (!isAuthentic) {
    return NextResponse.json({ success: false }, { status: 400 });
  }

  // TODO: Save payment to your database here
  // await db.payment.create({ data: { ... } })

  return NextResponse.json({ success: true });
}
The format
text
order_id|payment_id
is exact — no spaces, the pipe character is literal. If you mess this up, every signature will fail and you'll lose hours debugging.

Step 7 — Handle Razorpay Webhooks (the Step Most Tutorials Skip)

Webhooks are how Razorpay tells your server about events independently of the user's browser. Critical scenario: the user pays, then their phone dies before the redirect fires. Without webhooks, you'd never know they paid.

Set Up the Webhook Endpoint

Create
text
app/api/webhook/route.ts
:
typescript
import { NextRequest, NextResponse } from "next/server";
import crypto from "crypto";

export async function POST(req: NextRequest) {
  const body = await req.text(); // raw body, not parsed
  const signature = req.headers.get("x-razorpay-signature");

  const expectedSignature = crypto
    .createHmac("sha256", process.env.RAZORPAY_WEBHOOK_SECRET!)
    .update(body)
    .digest("hex");

  if (signature !== expectedSignature) {
    return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
  }

  const event = JSON.parse(body);

  switch (event.event) {
    case "payment.captured":
      // Mark the order as paid in your database
      await markOrderPaid(event.payload.payment.entity);
      break;
    case "payment.failed":
      await markOrderFailed(event.payload.payment.entity);
      break;
    case "refund.processed":
      await markRefundProcessed(event.payload.refund.entity);
      break;
  }

  return NextResponse.json({ received: true });
}

async function markOrderPaid(payment: any) {
  // your DB logic
}
async function markOrderFailed(payment: any) {
  // your DB logic
}
async function markRefundProcessed(refund: any) {
  // your DB logic
}

Register It in the Razorpay Dashboard

  1. Go to Settings → Webhooks.
  2. Click + Add New Webhook.
  3. URL:
    text
    https://yourdomain.com/api/webhook
    .
  4. Secret: paste the same value you put in
    text
    RAZORPAY_WEBHOOK_SECRET
    .
  5. Select events:
    text
    payment.captured
    ,
    text
    payment.failed
    ,
    text
    refund.processed
    .
For local testing, expose your dev server with ngrok:
text
ngrok http 3000
gives you a public URL you can paste into the dashboard.

Step 8 — Save the Payment to Your Database

Here's a minimal Prisma schema for storing payments:
prisma
model Payment {
  id              String   @id @default(cuid())
  razorpayOrderId String   @unique
  razorpayPaymentId String?
  amount          Int
  currency        String   @default("INR")
  status          String   // 'created' | 'paid' | 'failed' | 'refunded'
  userId          String?
  createdAt       DateTime @default(now())
  updatedAt       DateTime @updatedAt
}
Update the verify route to write to the database:
typescript
import { prisma } from "@/lib/prisma";

// inside the POST handler, after isAuthentic === true:
await prisma.payment.upsert({
  where: { razorpayOrderId: razorpay_order_id },
  update: { razorpayPaymentId: razorpay_payment_id, status: "paid" },
  create: {
    razorpayOrderId: razorpay_order_id,
    razorpayPaymentId: razorpay_payment_id,
    amount: 49900,
    status: "paid",
  },
});
Use
text
upsert
because the webhook might fire before the verify endpoint, or vice versa. You want the same outcome regardless of order.

Step 9 — Implement Refunds

Refunds are a single API call. Create
text
app/api/refund/route.ts
:
typescript
import { NextRequest, NextResponse } from "next/server";
import Razorpay from "razorpay";

const razorpay = new Razorpay({
  key_id: process.env.RAZORPAY_KEY_ID!,
  key_secret: process.env.RAZORPAY_KEY_SECRET!,
});

export async function POST(req: NextRequest) {
  const { paymentId, amount } = await req.json();

  try {
    const refund = await razorpay.payments.refund(paymentId, {
      amount, // in paise; omit for full refund
      speed: "normal", // or 'optimum' for instant (extra fee)
    });

    return NextResponse.json({ success: true, refundId: refund.id });
  } catch (err: any) {
    return NextResponse.json(
      { error: err.message },
      { status: 500 }
    );
  }
}
Protect this route with admin auth — you don't want users triggering their own refunds via DevTools.

Step 10 — Going Live: Test Mode → Live Mode Checklist

This is where most integrations break. Before you flip the switch:
  • Complete Razorpay KYC. Submit PAN, GST (if applicable), and bank details. Approval takes 1-3 business days.
  • Generate live API keys under the same Settings → API Keys page (with the toggle on Live).
  • Update environment variables in production — Vercel, Netlify, or wherever you deploy. Do not commit live keys to your repo.
  • Whitelist your production domain in the Razorpay dashboard under Settings → Configuration. Live mode rejects payments from unapproved domains.
  • Re-register your webhook with the live-mode URL (the dashboard has separate webhook lists for test and live).
  • Switch the checkout script if you hardcoded test branding anywhere.
  • Test with a real ₹1 payment first. Don't trust your test-mode logs.
  • Set up Slack/email alerts for
    text
    payment.failed
    webhook events so you catch issues early.

Common Errors and How to Fix Them

text
BAD_REQUEST_ERROR: amount is required
You forgot to multiply by 100. Razorpay wants paise, not rupees.
text
Signature verification failed
Either you used
text
key_id
instead of
text
key_secret
in the HMAC, or your concatenation format is wrong. It must be exactly
text
order_id|payment_id
.
text
Razorpay is not defined
The checkout script hasn't loaded yet. Use Next.js
text
&lt;Script>
with
text
strategy="afterInteractive"
or wait for
text
window.Razorpay
to exist.
Webhook signature mismatch You're parsing the body before hashing it. Use
text
req.text()
to get the raw body —
text
req.json()
will mutate whitespace and break the signature.
Payments work in test but fail in live Your live domain isn't whitelisted. Go to Settings → Configuration → Add Domain.

TypeScript Types for Razorpay

The
text
@types/razorpay
package covers the SDK, but the checkout options object on the client isn't typed. Drop this in
text
types/razorpay.d.ts
:
typescript
interface RazorpayOptions {
  key: string;
  amount: number;
  currency: string;
  name: string;
  description?: string;
  order_id: string;
  handler: (response: RazorpayResponse) => void;
  prefill?: { name?: string; email?: string; contact?: string };
  theme?: { color?: string };
}

interface RazorpayResponse {
  razorpay_payment_id: string;
  razorpay_order_id: string;
  razorpay_signature: string;
}
Now your IDE will autocomplete the options instead of you guessing.

FAQs

Is Razorpay free to integrate with Next.js? Yes. The SDK and integration are free. Razorpay charges a transaction fee (typically 2% + GST for domestic cards and UPI) only when a payment goes through.
Does Razorpay work with the Next.js App Router? Yes, fully. The examples in this guide use the App Router with Route Handlers and optionally Server Actions in Next.js 15.
Do I need a backend separate from Next.js? No. Next.js Route Handlers (or Server Actions) act as your backend. The Razorpay Node SDK runs in those server-side handlers without any extra service.
How do I test Razorpay payments without spending real money? Use test mode. Razorpay provides test card numbers like
text
4111 1111 1111 1111
with any future expiry and any CVV. UPI test ID:
text
success@razorpay
.
What's the difference between order_id and payment_id? The order ID is created by your server before the user pays. The payment ID is created by Razorpay after the user pays. You verify the link between them using the signature.
Why use webhooks if the handler already confirms payment? The handler runs in the user's browser and can fail (closed tab, network drop, browser crash). Webhooks are server-to-server and reliable. Use both.
Can I integrate Razorpay subscriptions in Next.js? Yes — the flow is similar but uses
text
razorpay.subscriptions.create()
instead of
text
orders.create()
. The signature verification format also changes slightly. That's a separate guide.
How long does Razorpay KYC take in 2026? Usually 1-3 business days if your documents are clean. Mismatched names between PAN and bank account is the #1 reason for delays.

Get the Code

Full working example with Tailwind UI, Prisma database, and webhook handling: github.com/your-handle/razorpay-nextjs-2026
Live demo (test mode, free to try): your-demo-url.vercel.app

What to Build Next

Now that you have payments working, the natural next steps are:
  • Subscriptions for recurring SaaS billing
  • Razorpay Route if you need to split payments between multiple sellers (marketplaces)
  • Saved cards / tokenization for one-tap repeat purchases
  • Invoice generation with
    text
    razorpay.invoices.create()
If this guide saved you a few hours, share it with a developer who's about to integrate Razorpay for the first time — they'll thank you.

Skip the setup and start shipping

Love this guide? All these patterns are pre-configured in our **SaaS Starter Pro** kit. Save 40+ hours of development.

Explore the Kit

Related Articles

Selected insights to level up your development workflow.

View all
Tutorial
5 min

How to Add Stripe to Next.js (2026)

A complete walkthrough of integrating Stripe Checkout and webhooks into your Next.js application.

Read more
Tutorial
8 min

Next.js + Prisma + Stripe Tutorial

Learn how to build a subscription-based SaaS using the powerhouse trio of Next.js, Prisma, and Stripe.

Read more
Tutorial
10 min

Next.js Authentication with Clerk — Complete Guide

Why Clerk is our top choice for SaaS authentication and how to set it up in under 15 minutes.

Read more
Browse all articles
Free for everyoneno signup · no credit card

Keep building with free resources

Production-ready starter kits and zero-friction developer tools — the same ones we use to ship our own products.

4 kits
9 tools

Starter Kits

clone · ship
FreeFeatured

Next.js Blog Kit

MDX-powered blog with full SEO, dark mode, RSS feed, reading time, and syntax highlighting. Deploy to Vercel in one click.

Next.jsMDXTailwind
Get kit

Landing Page Kit

Free

Conversion-optimised landing page with hero, pricing, testimonials, FAQ, waitlist form, and analytics integration built in.

Waitlist App

Free

Viral referral waitlist with position tracking, email confirmation, social share, and a live Supabase backend. Zero to launch in an hour.

Developer Tools

instant · in-browser
12k+
usage / mo

Shadcn/UI Component Previewer

Live preview of shadcn/ui components with instant copy-paste code. Browse rendered components and grab snippets.

Productivity
Open tool

Next.js Project Structure Generator

8.5k

Select your stack and instantly get a production-ready folder structure. Copy the entire scaffold in one click.

.env File Generator

24k

Pick your tech stack and get a complete, commented .env boilerplate file. Never forget an environment variable.

Tailwind CSS Color Palette Generator

15k+

Enter a brand color and generate a complete Tailwind-compatible shade scale with config snippets.

Looking for something specific?

Browse the full library — 7+ kits across 4+ categories.

Browse all resources
Back to blog
Share article