| name | stripe-migration |
| description | Migrate from Stripe to Razorpay — concept mapping, code migration patterns, parallel running. Use when the user asks to "migrate from stripe", "switch to razorpay", "move from stripe to razorpay", "run stripe and razorpay together", or needs to transition an existing Stripe billing system. |
| argument-hint | [mapping|migration-plan|parallel] |
Stripe to Razorpay Migration
Migrate an existing Stripe integration to Razorpay. Covers concept mapping, code patterns, parallel provider strategies, and gotchas that bite during the switch.
Concept Mapping
| Stripe | Razorpay | Notes |
|---|
| Customer | Customer | Similar API, different fields |
| Price / Product | Plan | Razorpay uses a single Plan object (no separate Price) |
| Subscription | Subscription | Different lifecycle states and params |
| PaymentIntent | Order | Razorpay Orders are simpler — no confirm step |
| Checkout Session | Hosted checkout (short_url) | Razorpay returns a URL on subscription creation |
| Webhook | Webhook | Different event names, different signature scheme |
stripe.webhooks.constructEvent() | Manual HMAC verification | See signature section below |
Stripe CLI (stripe listen) | ngrok + Razorpay Dashboard | No local CLI forwarding tool |
| Customer portal | No equivalent | Must build subscription management UI yourself |
Webhook Event Mapping
| Stripe Event | Razorpay Event | Notes |
|---|
checkout.session.completed | subscription.activated | Razorpay fires on first successful charge |
invoice.paid | subscription.charged | Recurring payment success |
invoice.payment_failed | payment.failed | Check subscription_id in payload to link |
customer.subscription.deleted | subscription.cancelled | Also check subscription.completed and subscription.halted |
customer.subscription.updated | subscription.updated | Detect plan changes via plan_id field |
charge.refunded | payment.refund.processed | Different payload structure |
Code Migration Patterns
SDK Initialization
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
import Razorpay from "razorpay";
const razorpay = new Razorpay({
key_id: process.env.RAZORPAY_KEY_ID!,
key_secret: process.env.RAZORPAY_KEY_SECRET!,
});
Subscription Creation
const session = await stripe.checkout.sessions.create({
mode: "subscription",
line_items: [{ price: priceId, quantity: 1 }],
success_url: "https://example.com/success",
cancel_url: "https://example.com/cancel",
});
const subscription = await razorpay.subscriptions.create({
plan_id: planId,
total_count: 60,
quantity: 1,
customer_notify: 1,
notes: { userId, planKey },
});
Webhook Signature Verification
const event = stripe.webhooks.constructEvent(
rawBody,
request.headers["stripe-signature"],
process.env.STRIPE_WEBHOOK_SECRET!
);
import crypto from "crypto";
const signature = request.headers.get("x-razorpay-signature");
const expected = crypto
.createHmac("sha256", process.env.RAZORPAY_WEBHOOK_SECRET!)
.update(rawBody)
.digest("hex");
const isValid = crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(signature!, "hex")
);
Checkout Redirect
window.location.href = session.url;
const popup = window.open(subscription.short_url, "_blank");
if (!popup || popup.closed) {
showFallbackLink(subscription.short_url);
}
Migration Strategy for Existing Subscribers
Do NOT migrate active Stripe subscriptions. Let them expire naturally.
- New signups go to Razorpay immediately
- Existing Stripe subscribers continue on Stripe until their subscription expires or cancels
- When a Stripe sub ends, redirect the user to Razorpay checkout on their next visit
- Run both providers in parallel during the transition period
Access Check (Parallel Providers)
async function hasActiveSubscription(userId: string): Promise<boolean> {
const [stripeSub, razorpaySub] = await Promise.all([
getActiveStripeSubscription(userId),
getActiveRazorpaySubscription(userId),
]);
return !!(stripeSub || razorpaySub);
}
Parallel Provider Pattern
Abstract billing behind an interface so your app code does not care which provider is active:
interface BillingProvider {
getActiveSubscription(userId: string): Promise<Subscription | null>;
createCheckout(userId: string, planKey: string): Promise<{ url: string }>;
cancelSubscription(subscriptionId: string): Promise<void>;
handleWebhook(request: Request): Promise<void>;
}
class StripeBilling implements BillingProvider { }
class RazorpayBilling implements BillingProvider { }
async function getBillingProvider(userId: string): Promise<BillingProvider> {
const stripeSub = await getActiveStripeSubscription(userId);
if (stripeSub) return new StripeBilling();
return new RazorpayBilling();
}
Webhook Routes (Both Active)
export async function POST(request: Request) {
}
export async function POST(request: Request) {
}
Keep both webhook routes active until all Stripe subscriptions have expired.
Key Differences That Bite You
- Charging model: Stripe charges automatically (pull-based). Razorpay subscriptions are similar, but hosted checkout is push-based (user initiates).
- Customer portal: Stripe has a built-in portal for managing subscriptions. Razorpay does not — you must build cancellation, plan change, and invoice UI yourself.
- Webhook signature header: Stripe uses
stripe-signature, Razorpay uses x-razorpay-signature.
- SDK types: Stripe SDK has excellent TypeScript types. Razorpay SDK types have quirks (e.g.,
fail_existing needs 0 as 0 | 1 cast).
- Currency units: Stripe uses cents (USD smallest unit), Razorpay uses paise (INR smallest unit). Both are smallest-unit integers, but watch currency conversion logic.
- Idempotency keys: Stripe supports idempotency keys natively on API calls. Razorpay does not — you must implement idempotency yourself (especially for webhooks).
Gotchas
- Never cancel all Stripe subscriptions at once — migrate gradually. Let subscriptions expire naturally or cancel in batches.
- Razorpay requires plans before subscriptions: You cannot create prices inline like Stripe. Create plans in the Razorpay Dashboard or via API before creating subscriptions.
- Razorpay webhook secret is separate from API secret: Stripe uses a webhook signing secret. Razorpay also has a separate webhook secret (configured in Dashboard), distinct from your
key_secret.
- Test modes are completely isolated: Test your Razorpay integration separately with test keys. Stripe test mode and Razorpay test mode are independent — do not mix credentials.
- Raw body is critical for both: Both providers compute webhook signatures on the raw request body. Parsing JSON before verification breaks the signature check.
- No
short_url retrieval: Razorpay's hosted checkout URL cannot be fetched after subscription creation. If the URL is lost, you must create a new subscription.
- Transition period database schema: Add a
provider column ("stripe" | "razorpay") to your subscriptions table to distinguish which provider manages each subscription.