Razorpay
Use Razorpay for MainSDK payments in India - UPI and cards, test and live keys, webhook secrets, market routing, local testing and swapping.
Razorpay is the India provider behind pay. Your app starts a checkout, a subscription or a refund through pay, and Razorpay's hosted checkout takes the payment by UPI, cards and the other methods you turn on. Pick Razorpay when you sell to customers in India. Most apps route only the India market to it and keep Stripe for everyone else, from one deployment.
Interfaces it serves
Pick Razorpay in main.yaml for any of these. Your calls stay the same, and you can swap later.
payBuilding for 0.3Driver razorpay Razorpay
payments:
driver: razorpaySwap to another provider
Before you start
You need a Razorpay account for your business in India. Razorpay gives every account a test mode and a live mode, each with its own keys and webhooks.
- Generate test API keys. In the Razorpay Dashboard, open the API keys area and generate a key pair for test mode: a key id and a key secret. The secret is shown once; copy it.
- Add a webhook. In the webhooks area, add a webhook that points at the URL where your app receives
payevents, and choose the payment, refund and subscription events your app handles. Howpay.on()handlers are mounted on a route is not defined yet. You set the webhook secret yourself when you create it; keep it. - Get live keys. Once your account is activated for live payments, generate a live key pair and add a live webhook with its own secret.
- Turn on payment methods. Make sure UPI and the other methods you plan to offer are enabled on your account.
Store the keys and secrets in your secrets manager, never in code or a committed env file.
Configure
Route the India market to Razorpay under payments:; its settings live under pay.razorpay.
payments: { in: razorpay, default: stripe }
pay:
razorpay:
key: secret:RAZORPAY_KEY # suggested name; the API key secret
webhook: secret:RAZORPAY_WEBHOOK_SECRET # suggested name; the webhook secret
methods: [upi, cards] # methods offered on checkoutRazorpay authenticates with a key id and a key secret. Put both in the one key secret as key_id:key_secret.
You can also keep the default provider in the base file and switch only in the India overlay:
payments: { driver: razorpay, methods: [upi, cards] }Both forms resolve per request. How they combine is not decided yet; see Payments. Use test-mode values in development and stage and live values in production. A literal key fails mainsdk check with MAIN-CONFIG-0004.
Use it
Amounts are integers in minor units, so 1,250 rupees is 125000 paise.
"use server";
import { auth, pay } from "mainsdk";
import { redirect } from "next/navigation";
export async function startCheckout(req: Request, orderId: string) {
await auth.require(req);
const session = await pay.checkout({
order: orderId,
amount: 125000, // integer minor units (paise)
currency: "INR", // ISO 4217
successUrl: "/orders/done",
cancelUrl: "/cart",
});
if ("url" in session) redirect(session.url);
return session; // { clientSecret } for hosted fields
}React to verified Razorpay events:
import { jobs, pay } from "mainsdk";
pay.on("payment.succeeded", async (event) => {
await jobs.run("orders.markPaid", event); // runs once per event, after the signature check
});Local development and testing
- No Razorpay account:
mainsdk devwith no provider key runs the local test card flow, which completes a checkout and firespayment.succeeded. - Razorpay test mode: use your test key and webhook secret to try the real hosted checkout with Razorpay's test cards and test UPI flows. To receive webhooks locally, expose your dev server through a tunnel and point a test webhook at it.
- Tests:
createTestApp()from@mainsdk/testingrunspayon its in-memory fake.chaos.on("pay", { outage: true })simulates a provider outage.
import { it } from "node:test";
import { createTestApp, expect } from "@mainsdk/testing";
it("charges an India order once in rupees", async () => {
const app = createTestApp({ main: "./main.yaml" });
// ... run your checkout action twice as an India user
expect(app.pay).toHaveCharged({ amount: 125000, currency: "INR", times: 1 });
});What MainSDK adds on top
- Checkout only on Razorpay Checkout; card numbers never touch your server, and card handling in your code fails
MAIN-PAY-0001. - Every Razorpay webhook verified (signature and the spec's timestamp tolerance) before your handler runs; unverified calls get 400 and are logged, and duplicates run your handler once.
- Idempotency keys on checkout, subscribe and refund, so a retry never charges or refunds twice.
- Amounts in integer minor units with an ISO 4217 currency.
See Payments for the full contract.
Limits and notes
- The Razorpay driver comes with the
payspec (phase 1, milestone 0.3). - Razorpay is for businesses in India charging customers in India. Route other markets to Stripe.
- Payment data is held by Razorpay. Your own order rows follow your
dbregions (US, EU or India). - Signed payment approvals for RBI flows come in phase 3.
pay.raw()returns the Razorpay client for featurespaydoes not wrap.mainsdk checklists every use.
Swap to or from it
Route the India market to Razorpay, or back to the default, in one line:
payments: { in: razorpay, default: stripe } # was { default: stripe }FAQ
How do I offer UPI? Add upi to pay.razorpay.methods and make sure UPI is enabled on your Razorpay account. The hosted checkout shows it.
Can one deployment serve India and the rest of the world? Yes. payments: { in: razorpay, default: stripe } picks the provider per request from the user's market.
Do I need a Razorpay account to start building? No. mainsdk dev runs a local test card flow with no key, and tests use the in-memory fake.
Next
- Payments and Webhooks
- Stripe and Paddle
- Swap a driver
Open questions
Was this page helpful?