Ready to Ship

Razorpay Integration Starter Kit

Production-ready Razorpay payments for Next.js 14+. Complete code, webhooks, TypeScript. From zero to live in 30 minutes.

01 What's Included

Razorpay Utility Library

Create orders, verify payments, validate webhooks β€” all typed and tested.

API Routes (3)

Order creation, payment verification, webhook handler β€” production-ready Next.js route handlers.

Payment Button Component

Drop-in React component with loading states, error handling, and success callbacks.

TypeScript Types

Full type definitions for Razorpay options, responses, orders, and webhooks.

Tech Stack

Prerequisites

02 Setup Guide

Step 1 β€” Create a Razorpay Account

  1. Go to razorpay.com and click "Sign Up"
  2. Enter your business email, set a password
  3. Complete KYC: PAN, Aadhaar, bank account details
  4. Account activates in 2–3 business days (test mode works instantly)
ℹ️ Test Mode You can start integrating immediately in test mode. No KYC needed for test keys.

Step 2 β€” Get API Keys

  1. Log into Razorpay Dashboard β†’ Settings β†’ API Keys
  2. Click "Generate Key Pair"
  3. Copy the Key ID (starts with rzp_test_) and Key Secret
  4. For live: repeat with Key ID starting rzp_live_
⚠️ Keep Key Secret safe Never commit it to git. Never expose it in client-side code. It stays server-side only.

Step 3 β€” Install Dependencies

Terminal
# Using npm
npm install razorpay

# Using pnpm
pnpm add razorpay

# Using yarn
yarn add razorpay

Step 4 β€” Environment Variables

Copy the .env.example file from the code section below to .env.local in your project root and fill in your keys.

.env.local
# Razorpay
RAZORPAY_KEY_ID=rzp_test_xxxxxxxxxxxxx
RAZORPAY_KEY_SECRET=xxxxxxxxxxxxxxxxxxxxxxxx
RAZORPAY_WEBHOOK_SECRET=your_webhook_secret_here

03 Code Files

a) lib/razorpay.ts β€” Razorpay Utility

lib/razorpay.ts
import Razorpay from "razorpay";
import crypto from "crypto";

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

interface CreateOrderParams {
  amount: number; // in INR (e.g., 999)
  currency?: string;
  receipt?: string;
  notes?: Record<string, string>;
}

export async function createOrder(params: CreateOrderParams) {
  const order = await razorpay.orders.create({
    amount: Math.round(params.amount * 100), // paisa
    currency: params.currency ?? "INR",
    receipt: params.receipt,
    notes: params.notes,
  });
  return order;
}

export function verifyPayment(
  orderId: string,
  paymentId: string,
  signature: string
): boolean {
  const body = orderId + "|" + paymentId;
  const expected = crypto
    .createHmac("sha256", process.env.RAZORPAY_KEY_SECRET!)
    .update(body)
    .digest("hex");
  return expected === signature;
}

export function verifyWebhook(
  body: string,
  signature: string
): boolean {
  const expected = crypto
    .createHmac("sha256", process.env.RAZORPAY_WEBHOOK_SECRET!)
    .update(body)
    .digest("hex");
  return expected === signature;
}

export default razorpay;

b) app/api/razorpay/order/route.ts β€” Create Order

app/api/razorpay/order/route.ts
import { NextResponse } from "next/server";
import { createOrder } from "@/lib/razorpay";

export async function POST(req: Request) {
  try {
    const { amount, receipt, notes } = await req.json();

    if (!amount || typeof amount !== "number" || amount < 1) {
      return NextResponse.json(
        { error: "Invalid amount" },
        { status: 400 }
      );
    }

    const order = await createOrder({
      amount,
      receipt ?? `rcpt_${Date.now()}`,
      notes: { ...notes, source: "nextjs-app" },
    });

    return NextResponse.json({
      orderId: order.id,
      amount: order.amount,
      currency: order.currency,
    });
  } catch (err) {
    console.error("Order creation failed:", err);
    return NextResponse.json(
      { error: "Failed to create order" },
      { status: 500 }
    );
  }
}

c) app/api/razorpay/verify/route.ts β€” Verify Payment

app/api/razorpay/verify/route.ts
import { NextResponse } from "next/server";
import { verifyPayment } from "@/lib/razorpay";

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

    if (!razorpay_order_id || !razorpay_payment_id || !razorpay_signature) {
      return NextResponse.json(
        { error: "Missing payment parameters" },
        { status: 400 }
      );
    }

    const isValid = verifyPayment(
      razorpay_order_id,
      razorpay_payment_id,
      razorpay_signature
    );

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

    // TODO: Update your database
    // await db.orders.update({
    //   where: { razorpayOrderId: razorpay_order_id },
    //   data: {
    //     status: "paid",
    //     razorpayPaymentId: razorpay_payment_id,
    //   },
    // });

    return NextResponse.json({ success: true });
  } catch (err) {
    console.error("Payment verification failed:", err);
    return NextResponse.json(
      { error: "Verification failed" },
      { status: 500 }
    );
  }
}

d) app/api/razorpay/webhook/route.ts β€” Webhook Handler

app/api/razorpay/webhook/route.ts
import { NextResponse } from "next/server";
import { verifyWebhook } from "@/lib/razorpay";

export async function POST(req: Request) {
  const body = await req.text();
  const signature = req.headers.get("x-razorpay-signature") ?? "";

  if (!verifyWebhook(body, signature)) {
    return NextResponse.json(
      { error: "Invalid webhook signature" },
      { status: 400 }
    );
  }

  const event = JSON.parse(body);

  switch (event.event) {
    case "payment.captured": {
      const payment = event.payload.payment.entity;
      // TODO: Fulfill order β€” send email, grant access, etc.
      console.log("Payment captured:", payment.id);
      break;
    }
    case "payment.failed": {
      const failed = event.payload.payment.entity;
      console.log("Payment failed:", failed.id);
      // TODO: Notify user, update order status
      break;
    }
    case "refund.created": {
      const refund = event.payload.refund.entity;
      console.log("Refund created:", refund.id);
      // TODO: Update order, notify user
      break;
    }
    default:
      console.log("Unhandled event:", event.event);
  }

  return NextResponse.json({ received: true });
}
ℹ️ Webhook Setup In Razorpay Dashboard β†’ Settings β†’ Webhooks, add your endpoint: https://yourdomain.com/api/razorpay/webhook. Select events: payment.captured, payment.failed, refund.created.

e) components/PaymentButton.tsx β€” Payment Component

components/PaymentButton.tsx
"use client";

import { useState } from "react";
import type { RazorpayOptions, RazorpayResponse } from "@/types/razorpay";

interface PaymentButtonProps {
  amount: number;
  name: string;
  description?: string;
  onSuccess?: (response: RazorpayResponse) => void | Promise<void>;
  onError?: (error: string) => void;
}

export default function PaymentButton({
  amount,
  name,
  description,
  onSuccess,
  onError,
}: PaymentButtonProps) {
  const [loading, setLoading] = useState(false);

  const handlePayment = async () => {
    setLoading(true);
    try {
      // 1. Create order
      const res = await fetch("/api/razorpay/order", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ amount, receipt: `rcpt_${Date.now()}` }),
      });
      const { orderId } = await res.json();
      if (!orderId) throw new Error("Failed to create order");

      // 2. Open Razorpay checkout
      const options: RazorpayOptions = {
        key: process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID!,
        amount: String(amount * 100),
        currency: "INR",
        name,
        description: description ?? name,
        order_id: orderId,
        handler: async (response: RazorpayResponse) => {
          // 3. Verify payment
          const verify = await fetch("/api/razorpay/verify", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(response),
          });
          const { success } = await verify.json();
          if (success) {
            onSuccess?.(response);
          } else {
            throw new Error("Payment verification failed");
          }
        },
        prefill: { method: "upi" },
        theme: { color: "#aa0404" },
      };

      const rzp = new window.Razorpay(options);
      rzp.on("payment.failed", (resp: any) => {
        const msg = resp.error?.description ?? "Payment failed";
        onError?.(msg);
      });
      rzp.open();
    } catch (err) {
      onError?.((err as Error).message ?? "Something went wrong");
    } finally {
      setLoading(false);
    }
  };

  return (
    <button
      onClick={handlePayment}
      disabled={loading}
      style={{
        background: loading ? "#555" : "#aa0404",
        color: "#fff",
        padding: "12px 32px",
        border: "none",
        borderRadius: "8px",
        fontSize: "1rem",
        cursor: loading ? "not-allowed" : "pointer",
        fontWeight: 600,
      }}
    >
      {loading ? "Processing…" : `Pay β‚Ή${amount}`}
    </button>
  );
}

f) types/razorpay.d.ts β€” TypeScript Types

types/razorpay.d.ts
export interface RazorpayOptions {
  key: string;
  amount: string;
  currency: string;
  name: string;
  description?: string;
  order_id: string;
  handler: (response: RazorpayResponse) => void;
  prefill?: {
    name?: string;
    email?: string;
    contact?: string;
    method?: "upi" | "card" | "netbanking" | "wallet";
  };
  theme?: { color?: string };
  modal?: { ondismiss?: () => void };
  notes?: Record<string, string>;
}

export interface RazorpayResponse {
  razorpay_order_id: string;
  razorpay_payment_id: string;
  razorpay_signature: string;
}

export interface OrderDetails {
  id: string;
  amount: number;
  currency: string;
  status: "created" | "attempted" | "paid";
  receipt?: string;
  notes?: Record<string, string>;
}

// Extend Window for Razorpay checkout
declare global {
  interface Window {
    Razorpay: new (options: RazorpayOptions) => {
      open: () => void;
      on: (event: string, cb: Function) => void;
    };
  }
}

g) .env.example β€” Environment Template

.env.example
# Razorpay β€” Server-side (NEVER expose to client)
RAZORPAY_KEY_ID=rzp_test_xxxxxxxxxxxxx
RAZORPAY_KEY_SECRET=xxxxxxxxxxxxxxxxxxxxxxxx
RAZORPAY_WEBHOOK_SECRET=your_webhook_secret_here

# Razorpay β€” Client-side (safe for browser)
NEXT_PUBLIC_RAZORPAY_KEY_ID=rzp_test_xxxxxxxxxxxxx

X How to Use This Product

  • Install dependencies: npm install razorpay in your Next.js 14+ project.
  • Copy files in order: Types β†’ Library β†’ API Routes β†’ Webhook β†’ Frontend.
  • Set up environment variables with your Razorpay test keys from the dashboard.
  • Test with Razorpay test mode (rzp_test_...) during development.
  • Register your webhook URL in Razorpay Dashboard β†’ Settings β†’ Webhooks.

X Use Cases

SaaS subscriptions

Set up recurring payments for monthly/annual subscription products.

Digital product sales

One-time payment integration for selling ebooks, courses, or templates.

E-commerce checkout

Complete payment flow with Razorpay for online stores.

Marketplace platforms

Handle payments from multiple sellers with split payments.

Event ticketing

Accept payments for workshops, webinars, or conferences.

X How This Helps You

  • Saves weeks of development β€” complete, production-ready code instead of scattered documentation.
  • TypeScript-first β€” full type safety catches errors before they reach production.
  • India-optimized β€” handles INR directly, Indian payment methods (UPI, NetBanking, Wallets).
  • Webhook handling included β€” payment status updates without polling.
  • Tested patterns β€” follows Razorpay's official recommendations with real-world additions.
🔒

Get the Complete Starter Kit

You've seen the code files. Unlock the full integration guide, troubleshooting table, and pre-launch checklists.

3
Integration Patterns
6
Issue Fixes
19
Checklist Items

04 Integration Guide

Product Page β€” One-time Payment

Use PaymentButton directly. Pass amount in INR (no paise conversion needed β€” the component handles it).

app/product/[id]/page.tsx
import PaymentButton from "@/components/PaymentButton";

export default function ProductPage() {
  return (
    <div>
      <h1>My Awesome Product</h1>
      <p>β‚Ή999 β€” Lifetime access</p>
      <PaymentButton
        amount={999}
        name="My Awesome Product"
        onSuccess={(res) => {
          window.location.href = `/success?order=${res.razorpay_order_id}`;
        }}
      />
    </div>
  );
}

Course / Subscription Payment

Pass additional notes for tracking. Use the webhook to grant access after confirmation.

components/CourseEnroll.tsx
"use client";
import { useState } from "react";
import PaymentButton from "@/components/PaymentButton";

export default function CourseEnroll({ courseId, price }: { courseId: string; price: number }) {
  const [status, setStatus] = useState<"idle" | "success">("idle");

  if (status === "success") {
    return <p>πŸŽ‰ Enrollment confirmed! Check your email.</p>;
  }

  return (
    <PaymentButton
      amount={price}
      name="Course Enrollment"
      onSuccess={async () => {
        // Webhook handles actual enrollment
        setStatus("success");
      }}
      onError={(msg) => alert(msg)}
    />
  );
}

Refund Handling

Process refunds via Razorpay Dashboard or API. The webhook (refund.created) auto-catches it.

Server-side refund (optional)
import razorpay from "@/lib/razorpay";

// Partial refund
await razorpay.payments.refund("pay_xxxxx", {
  amount: 50000, // β‚Ή500 in paisa
  notes: { reason: "Customer request" },
});

// Full refund
await razorpay.payments.refund("pay_xxxxx");

05 Common Issues & Fixes

IssueCauseFix
CORS error on checkout Razorpay script not loaded Add <script src="https://checkout.razorpay.com/v1/checkout.js"> in layout.tsx or load it dynamically before checkout
Webhook not receiving events Wrong URL or not publicly accessible Use ngrok for local testing. Ensure URL ends with /api/razorpay/webhook. Check webhook secret matches env var.
Payment captured but order not updated Webhook didn't fire or handler threw Check Razorpay Dashboard β†’ Webhooks β†’ Logs. Add error logging in webhook handler. Re-trigger from dashboard.
Test vs live key confusion Using live key in dev or test key in prod Use NODE_ENV to switch, or separate env files. Test keys = rzp_test_, live = rzp_live_
"Unauthorized" on order creation Missing or wrong key secret Double-check RAZORPAY_KEY_SECRET in .env.local. No trailing spaces.
Signature mismatch on verify Secret mismatch or body tampered Ensure RAZORPAY_KEY_SECRET is identical to dashboard. Verify body isn't modified between receipt and verification.
⚠️ UPI payments UPI transactions may take 2–3 seconds longer to confirm. Don't mark as failed immediately β€” the webhook will fire when the bank confirms.

06 Checklists

Pre-Launch Checklist

  • All API routes return proper error responses (not 500 stack traces)
  • Payment verification runs server-side (never trust client alone)
  • Webhook signature verification is active
  • Amount validation prevents zero/negative/overflow amounts
  • Environment variables are in .env.local, not committed to git
  • Razorpay script loaded before checkout opens
  • Test with all payment methods: UPI, cards, netbanking
  • Test failed payment flow (use test card 4111 1111 1111 1111)
  • Test payment dismissed by user (modal closed without paying)
  • Idempotency: duplicate webhook calls don't double-charge or double-fulfill

Go-Live Checklist

  • Switch from test to live keys in environment variables
  • Update webhook URL in Razorpay Dashboard to production domain
  • Verify webhook secret matches production env var
  • Enable live payment methods in Dashboard (UPI, Cards, Netbanking, Wallets)
  • Test one real β‚Ή1 transaction end-to-end
  • Verify email notifications fire on payment.captured
  • Check Razorpay Dashboard for successful webhook delivery
  • Set up Razorpay payout schedule for your bank account
  • Monitor first 24 hours for failed webhooks or signature errors

How to Use This Starter Kit

  1. Install dependencies: Run npm install razorpay in your Next.js 14+ project.
  2. Set up environment variables: Copy the env template and add your Razorpay test keys from the dashboard.
  3. Follow the file order: Types β†’ Library β†’ API Routes β†’ Webhook β†’ Frontend. Each file builds on the previous.
  4. Test mode first: Use Razorpay test keys (rzp_test_...) during development. Only switch to live keys when ready to go live.
  5. Webhook setup: Register your webhook URL in the Razorpay Dashboard β†’ Settings β†’ Webhooks. Select payment.captured and payment.failed events.

Tip: Click the "Copy" button on any code block to copy it instantly. All files use TypeScript and work with Next.js App Router (14+).