paymentsindiarazorpaynextjsfreelancingsaas

The Payment World: Complete Guide for Indian Developers

Mani Bharadwaj·
The Payment World: Complete Guide for Indian Developers

Everything you need to know about accepting payments in India — how it works, which gateway to use, when you get paid, and how to implement it in Next.js.


How the Payment World Works

Simple Version

Customer clicks "Pay ₹500"
       ↓
Your Next.js app sends "Create order for ₹500" to Payment Gateway
       ↓
Payment Gateway shows payment options (UPI, Card, Net Banking)
       ↓
Customer pays via UPI/Card/whatever
       ↓
Payment Gateway verifies with bank
       ↓
Payment Gateway tells your app "Payment successful ✅"
       ↓
Payment Gateway sends money to your bank account (in 1-3 days)

You never touch the customer's card. The gateway handles everything. You just say "create order" and "verify payment."

Detailed Flow

┌──────────┐     ┌──────────┐     ┌──────────────┐     ┌──────────┐
│ Customer  │────→│ Your App  │────→│   Razorpay   │────→│   Bank   │
│ (Browser) │←────│ (Next.js) │←────│   (Gateway)  │←────│ (SBI/etc)│
└──────────┘     └──────────┘     └──────────────┘     └──────────┘
     │                │                    │                    │
  1. Clicks     2. Creates order     3. Shows checkout    4. Processes
     "Pay"       on server           page (UPI/Card)     payment
     │                │                    │                    │
     │           5. Gateway calls   6. Gateway sends     7. Money settles
     │              webhook            signature            to your bank
     │                │                    │               (T+1 to T+3)
     │                │                    │
     └────────────────┴────────────────────┘
              8. Your app verifies
                 signature server-side
                 and marks order PAID

Which Payment Gateways India Uses

GatewayMarket PositionBest ForUPI?Next.js SDKDocs Quality
🥇 Razorpay#1, 50M+ businessesStartups, SaaS, Next.jsOfficial React/NodeExcellent
🥈 CashfreeGrowing fast, cheapestMarketplaces, payoutsOfficial JS SDKGood
🥉 PayUEnterprise favoriteE-commerce, EMICommunityAdequate
CCAvenueLegacy leaderGovt, educationNo official NPMDated
Stripe IndiaBest for USD/globalInternational SaaS❌ No UPIOfficial Next.jsBest-in-class
PhonePe PGUPI-nativeUPI-first appsLimitedGrowing

Developer Experience Rankings

GatewayDX ScoreAPI DesignDocsSandboxCommunity
Stripe5/5Best-in-classBest-in-classExcellentMassive
Razorpay5/5Stripe-inspiredExcellentExcellentLarge
Cashfree4/5Modern RESTGoodGoodGrowing
PayU3.5/5AdequateAdequateBasicModerate
CCAvenue2.5/5DatedDatedBasicSmall

Bottom line: Razorpay and Stripe tie for developer experience. Razorpay cloned Stripe's philosophy for India. CCAvenue's API feels like 2015.


Fees — What You Actually Pay

Domestic (India)

Payment MethodRazorpayCashfreePayUCCAvenue
UPI (under ₹2,000)0% 🎉0% 🎉0% 🎉1.90%
UPI (above ₹2,000)1.5%1.75%1.99%1.90%
Credit/Debit Cards2%1.75-1.9%1.99-2.5%1.90%
Net Banking₹5-15/txn₹6-12/txn₹5-14/txn1.90%
International Cards3-3.5%2.5-3.5%3.5-3.9%3%
Setup FeeFreeFreeFreeFree
Annual Fee₹1,999/yr (waivable)Free₹2,999/yr (waivable)₹1,200/yr (1st yr free)
Refund Fee₹100/refund₹50/refund₹100/refundNot listed
Chargeback Fee₹100/dispute₹150/dispute₹200/disputeNot listed

Remember: All fees are pre-GST. Add 18% GST on top.

Example: Customer pays ₹500 via UPI (under ₹2,000):

  • You pay: ₹0 in fees (UPI is free under ₹2K)
  • You receive: ₹500 in your bank (after T+2 settlement)

Example: Customer pays ₹5,000 via Credit Card:

  • Razorpay fee: 2% of ₹5,000 = ₹100
  • GST on fee: 18% of ₹100 = ₹18
  • Total fee: ₹118
  • You receive: ₹5,000 - ₹118 = ₹4,882

International (USD/EUR/GBP)

FeatureRazorpay MoneySaverCashfreeCCAvenueStripe India
Fee1% flat, 0% FX2.69-2.99%3.5%4.3% + 2% FX
Currencies135+140+27135+
e-FIRAAuto-generatedOn-demandAvailable❌ Not auto
SettlementT+1 in INRT+2 to T+3T+3 to T+5T+5 to T+7+
PA-CB License✅ Yes✅ Yes✅ Yes❌ No

Winner for international: Razorpay MoneySaver Export Account. 1% flat fee, zero forex markup, auto e-FIRA, T+1 settlement.


When Do You ACTUALLY Get Your Money?

Settlement Timelines

GatewayClaimedReality (Cards)Instant SettlementInternational
CashfreeT+1T+1 to T+2Yes (₹5 + 1%)T+2 to T+3
RazorpayT+2T+2 (often T+3)Yes (₹5 + 1.5%)T+3
PayUT+2T+2 to T+3Yes (₹10 + 2%)T+5
CCAvenueT+2-3T+2 to T+5Yes (on request)T+3 to T+5

What T+2 Actually Means

Customer Pays OnMoney Arrives On
MondayWednesday
TuesdayThursday
WednesdayFriday
ThursdayMonday (weekend + bank holiday)
FridayTuesday
SaturdayTuesday
SundayTuesday

If there's a bank holiday on Monday, add 1 more day.

Instant Settlement (When You Need Money TODAY)

GatewayFeeHow Fast
Razorpay₹5 + 1.5%Within 30 minutes
Cashfree₹5 + 1%Within 30 minutes
PayU₹10 + 2%Within 30 minutes

Worth it when: You need cash today. Not worth it for every transaction — the fees add up.


UPI — The King of Indian Payments

Why UPI Matters

Payment MethodVolume ShareValue ShareTrend
UPI86%~9.5%📈 Dominant and growing
Credit Cards~5%High (avg ₹4K)📈 Growing at 21% CAGR
Debit CardsDecliningDeclining📉 -24.4% CAGR
Net BankingDecliningModerate📉 Being replaced by UPI
WalletsDecliningSmall📉 Consolidating
EMI~2-3%Growing📈 18-25% AOV lift

9 out of 10 digital payments in India are UPI. If you're building for India, UPI is non-negotiable.

UPI Rules You Need to Know

RuleWhat It Means
UPI under ₹2,000 = 0% MDRYou pay zero fees on small UPI transactions
UPI over ₹2,000 = 1.5-2%You pay merchant discount rate
UPI AutoPay time restrictionsOnly processes before 10 AM, 1-5 PM, after 9:30 PM (since Aug 2025)
UPI AutoPay above ₹15,000Requires Additional Factor of Authentication (AFA)
24-hour pre-debit notificationMandatory before every auto-charge
Max 3 retriesPer failed recurring payment
Card tokenization mandatoryNo raw PAN storage since 2022 — gateway handles this

My Pick: Razorpay

Why Razorpay Wins for India

  1. Best developer experience — API designed like Stripe (clean, modern)
  2. Best docs — excellent Next.js tutorials everywhere
  3. UPI works perfectly — 86% of Indian payments are UPI
  4. Fastest KYC — minutes with cKYC/Aadhaar
  5. Subscriptions — UPI AutoPay + card e-Mandates
  6. International — MoneySaver Export Account for USD (1% flat, 0% forex)
  7. Free setup — no annual fee (waivable)
  8. 50M+ businesses — most popular in India for a reason

When to Use Something Else

If You NeedUse This
Cheapest feesCashfree (1.75% vs 2% on cards)
Fastest settlementCashfree (T+1 default)
Marketplace payoutsCashfree (best Payouts API)
International SaaS subscriptionsStripe (best multi-currency, best billing)
Enterprise EMIPayU (best EMI coverage, LazyPay BNPL)
Global customers, non-INRStripe (best multi-currency)

KYC Requirements

What You Need to Sign Up

RequirementRazorpayCashfreePayU
Aadhaar/DigiLocker
PANPersonal + BusinessVia APIBusiness PAN
Bank Verification₹1 depositBank statementIFSC + linking
Business ProofGST/MSME/IEC10+ doc typesGST/license
GST CertificateRequired or MSME alternativeAs business proofOptional at onboarding
Video KYCOnly if cKYC failsNot mentionedNot mentioned
Activation SpeedMinutes (with cKYC)Hours1-3 days

For Freelancers/Sole Proprietors

You need:

  • Personal PAN ✅
  • Aadhaar ✅
  • Bank details ✅
  • Business proof: GST certificate OR MSME/UDYAM registration OR Shop Establishment Certificate ✅

GST is NOT strictly required if you have MSME/UDYAM registration.

Fastest path: Razorpay with cKYC — activated in minutes.


International Payments (USD from Clients Abroad)

Razorpay MoneySaver Export Account

FeatureDetails
Fee1% flat (no forex markup!)
Virtual accountsUSD (ACH), GBP (FPS), EUR (SEPA), SWIFT
SettlementT+1 in INR
e-FIRAAuto-generated
KYCVideo KYC, minutes
Works withUpwork, Deel, Amazon

Example: Client pays you $1,000

$1,000 received
  → 1% fee: $10
  → FX conversion at live rate (no markup)
  → You receive: ~₹83,400 in your bank
  → e-FIRA auto-generated for tax filing

Comparison for Freelancers

PlatformFeeFX MarkupSettlemente-FIRA
Razorpay MoneySaver1% flat0%T+1Auto
Cashfree International2.69-2.99%1% + GSTT+2-3On-demand
PayPal4.4% + fixed fee3-4% hidden1-5 daysManual
Wise (TransferWise)0.5-1%Live rate1-2 daysManual
Stripe India4.3% + 2% FX2%T+5-7+❌ Not auto

PayPal is the WORST for freelancers. 4.4% + fixed fee + 3-4% hidden FX markup = ~8-9% total cost. Avoid.


Recurring Payments & Subscriptions

FeatureRazorpayCashfreePayUStripe India
UPI AutoPay
Card e-Mandates✅ (Visa/MC/RuPay)✅ (Visa/MC/RuPay)Limited
Subscriptions DashboardBest-in-classGoodBasicExcellent
ProrationLimited
Dunning ManagementLimited
Smart Retry✅ (optimal windows)Limited
Max UPI Recurring₹15K (std) / ₹1L (select)₹15K / ₹1L₹15KN/A

Key RBI e-Mandate Rules (2025-2026)

  • 24-hour pre-debit notification mandatory before every auto-charge
  • Customers can pause, revoke, or modify mandates from any UPI app
  • UPI AutoPay only processes during non-peak hours (before 10 AM, 1-5 PM, after 9:30 PM)
  • Max 3 retries per failed attempt
  • Transactions above ₹15,000 require AFA (Additional Factor of Authentication)

Next.js Implementation — Full Code

Step 1: Install

npm install razorpay

Step 2: Environment Variables

# .env.local
RAZORPAY_KEY_ID=rzp_test_XXXXXXXXXXXX
RAZORPAY_KEY_SECRET=XXXXXXXXXXXXXXXXXXXX
NEXT_PUBLIC_RAZORPAY_KEY_ID=rzp_test_XXXXXXXXXXXX

# For webhooks (set this in Razorpay Dashboard too)
RAZORPAY_WEBHOOK_SECRET=your_webhook_secret_here

Step 3: Create Order (Server Side)

// app/api/payments/create-order/route.ts
import Razorpay from 'razorpay'
import { NextResponse } from 'next/server'

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

export async function POST(request: Request) {
  try {
    const { amount, currency = 'INR' } = await request.json()

    // ⚠️ ALWAYS compute amount server-side from your DB
    // NEVER trust client-sent amounts
    // Amount is in PAISE: ₹500 = 50000

    const order = await razorpay.orders.create({
      amount: amount * 100, // Convert rupees to paise
      currency,
      receipt: `receipt_order_${Date.now()}`,
    })

    return NextResponse.json({ orderId: order.id }, { status: 200 })
  } catch (error) {
    console.error('Order creation failed:', error)
    return NextResponse.json(
      { error: 'Failed to create order' },
      { status: 500 }
    )
  }
}

Step 4: Payment Page (Client Side)

// app/payment/page.tsx
'use client'

import Script from 'next/script'
import { useState } from 'react'

declare global {
  interface Window {
    Razorpay: new (options: any) => any
  }
}

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

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

    try {
      // 1. Create order on server
      const res = await fetch('/api/payments/create-order', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ amount: 500 }), // ₹500
      })
      const { orderId } = await res.json()

      // 2. Open Razorpay checkout
      const options = {
        key: process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID,
        amount: 50000, // paise
        currency: 'INR',
        name: 'Your App Name',
        description: 'Product/Service description',
        order_id: orderId,
        handler: async function (response: any) {
          // 3. Verify payment on server
          const verifyRes = await fetch('/api/payments/verify', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(response),
          })
          const data = await verifyRes.json()

          if (data.success) {
            alert('Payment successful! ✅')
            // Redirect to success page or update UI
          } else {
            alert('Payment failed! ❌')
          }
        },
        prefill: {
          name: 'Mani Bharadwaj',
          email: 'manibharadwajcr@gmail.com',
          contact: '9632453556',
        },
        theme: {
          color: '#6366f1',
        },
      }

      const razorpay = new window.Razorpay(options)
      razorpay.open()
    } catch (error) {
      console.error(error)
      alert('Something went wrong')
    } finally {
      setLoading(false)
    }
  }

  return (
    <>
      <Script
        src="https://checkout.razorpay.com/v1/checkout.js"
        strategy="lazyOnload"
      />
      <div className="flex flex-col items-center justify-center min-h-screen gap-4">
        <h1 className="text-3xl font-bold">Pay ₹500</h1>
        <button
          onClick={handlePayment}
          disabled={loading}
          className="px-6 py-3 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50"
        >
          {loading ? 'Processing...' : 'Pay Now'}
        </button>
      </div>
    </>
  )
}

Step 5: Verify Payment (Server Side) — THE CRITICAL STEP

// app/api/payments/verify/route.ts
import { NextResponse } from 'next/server'
import crypto from 'crypto'

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

    // ⚠️ CRITICAL: Always verify the signature server-side
    // This proves the payment is real — NEVER skip this
    const body = `${razorpay_order_id}|${razorpay_payment_id}`

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

    // Use timingSafeEqual — NEVER use === for signature comparison
    const isValid = crypto.timingSafeEqual(
      Buffer.from(expectedSignature),
      Buffer.from(razorpay_signature)
    )

    if (!isValid) {
      return NextResponse.json(
        { success: false, message: 'Invalid signature' },
        { status: 400 }
      )
    }

    // Payment verified! ✅
    // Update your database here: mark order as paid
    // await db.order.update({ where: { id: orderId }, data: { status: 'PAID' } })

    return NextResponse.json({ success: true })
  } catch (error) {
    console.error('Verification failed:', error)
    return NextResponse.json(
      { success: false, message: 'Verification failed' },
      { status: 500 }
    )
  }
}

Step 6: Webhook (For Reliable Payment Updates)

// app/api/payments/webhook/route.ts
import { NextResponse } from 'next/server'
import crypto from 'crypto'

export async function POST(request: Request) {
  try {
    const body = await request.text()
    const signature = request.headers.get('x-razorpay-signature')!

    // Verify webhook signature
    const expectedSignature = crypto
      .createHmac('sha256', process.env.RAZORPAY_WEBHOOK_SECRET!)
      .update(body)
      .digest('hex')

    const isValid = crypto.timingSafeEqual(
      Buffer.from(expectedSignature),
      Buffer.from(signature)
    )

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

    const event = JSON.parse(body)

    // Handle different events
    switch (event.event) {
      case 'payment.captured':
        // ✅ Payment successful — update DB
        const paymentId = event.payload.payment.entity.id
        const orderId = event.payload.payment.entity.order_id
        const amount = event.payload.payment.entity.amount / 100 // Convert paise to rupees
        console.log(`Payment captured: ${paymentId} for ₹${amount}`)
        // await db.order.update({ where: { orderId }, data: { status: 'PAID' } })
        break

      case 'payment.failed':
        // ❌ Payment failed — update DB
        console.log('Payment failed:', event.payload.payment.entity.id)
        // await db.order.update({ where: { orderId }, data: { status: 'FAILED' } })
        break

      case 'refund.processed':
        // 💰 Refund processed — update DB
        console.log('Refund processed:', event.payload.refund.entity.id)
        break

      case 'subscription.charged':
        // 🔄 Recurring payment successful
        console.log('Subscription charged:', event.payload.subscription.entity.id)
        break

      case 'subscription.cancelled':
        // 🚫 Subscription cancelled
        console.log('Subscription cancelled')
        break
    }

    return NextResponse.json({ received: true })
  } catch (error) {
    console.error('Webhook failed:', error)
    return NextResponse.json({ error: 'Webhook failed' }, { status: 500 })
  }
}

Step 7: Subscription Payment (Recurring)

// app/api/payments/create-subscription/route.ts
import Razorpay from 'razorpay'
import { NextResponse } from 'next/server'

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

export async function POST(request: Request) {
  try {
    // Create a subscription plan first (or use an existing plan_id)
    const subscription = await razorpay.subscriptions.create({
      plan_id: 'plan_XXXXXXXXXXXX', // Create this in Razorpay Dashboard
      customer_notify: 1,
      total_count: 12, // 12 months
      quantity: 1,
    })

    return NextResponse.json({
      subscriptionId: subscription.id,
    })
  } catch (error) {
    return NextResponse.json({ error: 'Failed' }, { status: 500 })
  }
}
// Client side for subscription
const options = {
  key: process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID,
  subscription_id: subscriptionId, // From create-subscription API
  name: 'Your App Name',
  description: 'Monthly subscription',
  handler: async function (response: any) {
    // Verify subscription payment
    const res = await fetch('/api/payments/verify', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(response),
    })
    const data = await res.json()
    if (data.success) {
      alert('Subscription activated! ✅')
    }
  },
}

Critical Gotchas

GotchaWhat It MeansFix
Amounts are in PAISE₹500 = 50000 in the APIAlways multiply by 100
Never trust client amountsClient could send ₹1 for a ₹500 productAlways compute server-side from your DB
Use timingSafeEqual=== is vulnerable to timing attacksAlways use crypto.timingSafeEqual
T+2 means BUSINESS daysThursday payment = Monday settlementPlan cash flow accordingly
UPI AutoPay time restrictionsOnly processes before 10 AM, 1-5 PM, after 9:30 PMInform users about this
UPI over ₹15K needs AFAAdditional Factor of Authentication requiredNot your problem — gateway handles it
Card tokenization mandatoryNo raw PAN storage since 2022Gateway handles this automatically
GST on feesAll quoted fees are pre-GSTAdd 18% when calculating total cost
Webhooks are source of truthClient callbacks can be spoofedAlways update DB from webhooks
Handle "user closed modal"Customer closes payment without payingShow "Payment pending" state, don't assume failure
Razorpay Collect deprecatingUPI Collect flow deprecated Feb 2026Use UPI Intent flow instead

Your Play — Start to Running

Day 1: Sign Up

  1. Go to razorpay.com → Create account
  2. Complete KYC (Aadhaar + PAN + bank details) — takes 5-10 minutes
  3. Get your test API keys from Dashboard → Settings → API Keys

Day 1-2: Test Mode

  1. Use rzp_test_ keys
  2. Copy the code from this guide into your Next.js app
  3. Make test payments with Razorpay's test cards:
Card NumberTypeResult
4111 1111 1111 1111VisaSuccess
5111 1111 1111 1118MastercardSuccess
4111 1111 1111 1112VisaFailure
  1. Test UPI with success@razorpay UPI ID
  2. Verify everything works in test mode

Day 2-3: Go Live

  1. Complete full KYC (GST or MSME/UDYAM certificate)
  2. Switch to rzp_live_ keys
  3. Set up webhook URL in Razorpay Dashboard (Settings → Webhooks)
  4. Make a real ₹1 payment to test
  5. You're live. Taking payments.

Day 3-5: When Money Arrives

  • First payment → T+2 settlement (2-3 business days)
  • Need money faster? Enable instant settlement (₹5 + 1.5%)
  • Money lands in your bank account automatically
  • Check Razorpay Dashboard → Settlements → see all settlements

Freelance Platform Payment Comparison

PlatformHow You Get PaidFeeMin WithdrawalSpeedIndia?
UpworkPayPal, bank, Payoneer10%$55-14 days
ContraLocal bank (INR), PayPal, Payoneer, USDC0%None2-9 days
ToptalPayPal, bank, wire0%NoneBi-weekly
TuringBank transfer (USD)0% (hidden 50% margin)NoneBi-monthly✅✅
HackerOnePayPal, bank, Bitcoin0%NoneDays-weeks
AlgoraUSD escrow, 120+ countriesNot disclosedNot disclosedOn merge

Best Method for Indian Freelancers Accepting USD

Razorpay MoneySaver Export Account:

  • 1% flat fee, zero forex markup
  • Virtual accounts in USD (ACH), GBP (FPS), EUR (SEPA), SWIFT
  • Auto e-FIRA for tax filing
  • T+1 settlement in INR
  • Works with Upwork, Deel, direct clients

Avoid PayPal for receiving: 4.4% + fixed fee + 3-4% hidden FX = ~8-9% total cost. Razorpay MoneySaver at 1% flat is 8-9x cheaper.


Sources


The payment world is simple once you understand it: Customer pays → Gateway verifies → Gateway sends money to your bank. Everything else is just choosing the right gateway and implementing the code. Now go build. 🎮