Skip to Content
ProvidersRazorpay

Razorpay

Category: Payments
Integration type: Platform-level (single Razorpay account for all tenants)
External SDK: razorpay (official Node.js SDK)


Purpose

Razorpay handles all payment processing for Leadmetrics:

  1. Subscription billing — Monthly/annual plan payments (Free/Pro/Agency/Enterprise)
  2. Credit top-ups — On-demand credit purchases beyond plan allocation
  3. Invoice generation — Automatic invoices linked to each payment

Razorpay is a platform-level integration — there is one Razorpay account owned by the platform operator. Tenants never configure their own Razorpay keys.


Config Structure

Platform config (env vars)

RAZORPAY_KEY_ID=rzp_live_xxxxxxxxxxxxxxxxxxxx RAZORPAY_KEY_SECRET=xxxxxxxxxxxxxxxxxxxxxxxx RAZORPAY_WEBHOOK_SECRET=xxxxxxxxxxxxxxxxxxxxxxxx # For webhook signature verification

For test/staging:

RAZORPAY_KEY_ID=rzp_test_xxxxxxxxxxxxxxxxxxxx RAZORPAY_KEY_SECRET=xxxxxxxxxxxxxxxxxxxxxxxx

Integration Pattern

Plan subscription flow

Subscriptions are created in Razorpay and linked to tenant records:

import Razorpay from 'razorpay'; const razorpay = new Razorpay({ key_id: config.RAZORPAY_KEY_ID, key_secret: config.RAZORPAY_KEY_SECRET, }); // Create a plan (one-time setup — plans don't change often) async function createPlan(options: { name: string; // e.g. "Leadmetrics Pro Monthly" amount: number; // In paise (INR × 100): 4999 rupees = 499900 paise currency: string; // 'INR' interval: number; // 1 period: string; // 'monthly' | 'yearly' }): Promise<string> { const plan = await razorpay.plans.create({ period: options.period, interval: options.interval, item: { name: options.name, amount: options.amount, currency: options.currency, }, }); return plan.id; } // Subscribe a tenant to a plan async function createSubscription( planId: string, totalCount: number, // Number of billing cycles (e.g. 12 for annual) ): Promise<{ subscriptionId: string; shortUrl: string }> { const subscription = await razorpay.subscriptions.create({ plan_id: planId, total_count: totalCount, quantity: 1, notify_info: { notify_phone: tenantPhone, notify_email: tenantEmail, }, }); return { subscriptionId: subscription.id, shortUrl: subscription.short_url, // Payment page URL }; }

Credit top-up order flow

async function createTopupOrder(options: { credits: number; // Number of credits being purchased amount: number; // Amount in paise tenantId: string; }): Promise<{ orderId: string; amount: number; currency: string }> { const order = await razorpay.orders.create({ amount: options.amount, currency: 'INR', receipt: `topup-${options.tenantId}-${Date.now()}`, notes: { tenantId: options.tenantId, credits: String(options.credits), type: 'credit_topup', }, }); // Also insert a pending row in credit_topup_orders await db.insert(creditTopupOrders).values({ tenantId: options.tenantId, orderId: order.id, credits: options.credits, amountPaise: options.amount, status: 'pending', }); return { orderId: order.id, amount: order.amount, currency: order.currency, }; }

Webhook handling

Razorpay sends webhooks for payment events. All events go through signature verification before processing:

// POST /api/webhooks/razorpay async function handleRazorpayWebhook(req: Request): Promise<void> { const signature = req.headers['x-razorpay-signature'] as string; const isValid = Razorpay.validateWebhookSignature( JSON.stringify(req.body), signature, config.RAZORPAY_WEBHOOK_SECRET, ); if (!isValid) throw new UnauthorizedError('Invalid webhook signature'); const event = req.body as RazorpayWebhookEvent; switch (event.event) { case 'payment.captured': await handlePaymentCaptured(event.payload.payment.entity); break; case 'subscription.charged': await handleSubscriptionCharged(event.payload.subscription.entity); break; case 'subscription.cancelled': await handleSubscriptionCancelled(event.payload.subscription.entity); break; case 'payment.failed': await handlePaymentFailed(event.payload.payment.entity); break; } }

Credit top-up on payment.captured

async function handlePaymentCaptured(payment: RazorpayPayment): Promise<void> { const { tenantId, credits } = payment.notes; // Update order status await db.update(creditTopupOrders) .set({ status: 'paid', razorpayPaymentId: payment.id, paidAt: new Date() }) .where(eq(creditTopupOrders.orderId, payment.order_id)); // Add credits to balance await topupCredits(tenantId, Number(credits), payment.id); // Generate invoice via Billing Agent await billingQueue.add('generate-invoice', { tenantId, paymentId: payment.id }); // Notify tenant await notifyTenant(tenantId, 'credit_topup_confirmed', { credits }); }

Testing in Test Mode

Use rzp_test_* keys in .env. In test mode, no real money moves.

Test credentials — Razorpay Checkout (browser / E2E)

MethodCredentialNotes
UPI IDsuccess@razorpaySimulates instant success with no OTP. Best for browser/E2E testing.
UPI IDfailure@razorpaySimulates UPI payment failure.
Card (no 3DS)5267 3181 8797 5449 / exp 02/26 / CVV 123Domestic Mastercard — completes without OTP popup.
Card (3DS)4208 3000 9609 2278 / OTP 1234Visa 3DS — opens a Razorpay-hosted OTP screen within the same tab.
Card (avoid)4111 1111 1111 1111Opens an external 3DS bank popup — Playwright cannot interact with it; use the cards above instead.

E2E Playwright advice: Use success@razorpay as the UPI ID. Switch to UPI tab in the Razorpay modal, type the UPI ID in the “Pay with UPI ID / Number” field, click “Verify and Pay”. The modal will show “Confirming Payment” then “Payment Successful” and auto-redirect after 4 seconds.


Test Cases

Unit tests (packages/billing/src/razorpay.test.ts)

TestApproach
createTopupOrder() creates Razorpay order with correct amountMock razorpay.orders.create; assert amount, currency, receipt
createTopupOrder() inserts pending row in DBAssert credit_topup_orders row created with status: 'pending'
handleRazorpayWebhook() rejects invalid signaturePass wrong signature; assert 401
handlePaymentCaptured() credits tenant on paymentMock payment.captured event; assert credit_balances updated
handlePaymentFailed() sends failure notificationMock failed payment; assert notifyTenant called

Integration tests

TestApproach
Create order and simulate capture via Razorpay test modeUse rzp_test_* keys; create order; simulate capture via Razorpay dashboard
Webhook signature validationUse test secret; sign payload; assert accepted

Currency and Pricing

All amounts are stored and transmitted in paise (INR × 100):

PlanMonthly (INR)Amount in paise
Pro₹4,999499900
Agency₹12,9991299900
EnterpriseCustom

Credit top-up bundles:

CreditsPricePaise
10₹49949900
50₹1,999199900
150₹4,999499900

© 2026 Leadmetrics — Internal use only