Payment integration is the point where ecommerce theory meets real money. A Medusa.js store can have the most elegant product catalog, the fastest storefront, and the most thoughtful checkout UX, but if the payment gateway is misconfigured, your conversion rate drops to zero. Getting this step right matters more than almost anything else in the implementation.
This guide covers the complete setup for both Stripe and Razorpay in a Medusa.js v2 application. Stripe is the default choice for international stores, particularly those serving Europe and North America. Razorpay is the dominant payment gateway for Indian ecommerce, supporting UPI, NetBanking, Wallets, credit cards, and EMI schemes that Stripe does not yet fully cover for Indian merchants. Many Medusa.js stores serving Indian and global customers run both simultaneously, with the appropriate provider activated per region.
This guide assumes you have a working Medusa.js v2 application already running. If you are starting from scratch, the Medusa.js Architecture Explained guide on Askan Ecomm is a good foundation before diving into payment configuration.
How Medusa.js Handles Payments: A Quick Primer
How Medusa.js Handles Payments: A Quick Primer
Before touching any configuration, it helps to understand how Medusa.js structures payment processing. The platform uses a Payment Module with a provider-based architecture. Each payment gateway, such as Stripe or Razorpay, is registered as a Payment Module Provider. Medusa then exposes those providers to regions in your store, and customers see only the payment options available in their region.
When a customer initiates checkout, Medusa creates a Payment Session tied to the selected provider. The session holds the authorization state of the payment through the checkout flow. Once the customer completes payment on the frontend, the provider sends a confirmation back to Medusa via webhook, and Medusa captures or authorizes the payment accordingly. Refunds, partial captures, and cancellations all flow through the same Payment Module so that every provider behaves consistently from the admin's perspective.
The webhook handler endpoint Medusa provides follows the pattern /hooks/payment/{provider_identifier}_{provider_id}. For Stripe with the default configuration this becomes /hooks/payment/stripe_stripe. You point your payment provider's webhook settings at this URL, and Medusa handles the rest. Full details are at docs.medusajs.com/resources/commerce-modules/payment/webhook-events.
Part 1: Setting Up Stripe in Medusa.js v2
Part 1: Setting Up Stripe in Medusa.js v2
Step 1: Obtain Your Stripe API Keys
Log in to your Stripe dashboard and navigate to Developers, then API Keys. You will find two keys: the Secret Key, which stays on the server and never reaches the browser, and the Publishable Key, which is safe to expose in your storefront. Copy both. In Stripe's test mode, the keys begin with sk_test_ and pk_test_. Swap these for sk_live_ and pk_live_ keys when you go to production.
Step 2: Configure the Stripe Provider in medusa-config.ts
The Stripe Module Provider ships with Medusa.js by default. You do not need to install any additional package. Register it in the payment module's providers array inside your medusa-config.ts:
modules: [
{
resolve: "@medusajs/medusa/payment",
options: {
providers: [
{
resolve: "@medusajs/medusa/payment-stripe",
id: "stripe",
options: {
apiKey: process.env.STRIPE_API_KEY,
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
},
},
],
},
},
],
Step 3: Set Environment Variables
Add the following to your backend .env file:
STRIPE_API_KEY=sk_test_your_secret_key_here
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret_hereFor your Next.js storefront, add the publishable key to the storefront's .env.local:
NEXT_PUBLIC_STRIPE_KEY=pk_test_your_publishable_key_hereStep 4: Enable Stripe in Regions
Restart your Medusa backend after saving the configuration. Then open the Medusa Admin dashboard, navigate to Settings, then Regions, and select each region where you want Stripe to be available. In the Payment Providers section of that region, add Stripe and save. Your Next.js storefront will now display Stripe as a checkout option for customers in those regions.
Step 5: Configure Stripe Webhooks
For production deployments, Stripe must be able to notify your Medusa backend when payment events occur. In your Stripe dashboard, go to Developers, then Webhooks, and add a new endpoint. The URL should be your production Medusa backend URL followed by /hooks/payment/stripe_stripe. Select the following events to listen for:
payment_intent.succeeded
payment_intent.payment_failed
payment_intent.canceled
payment_intent.amount_capturable_updated
After saving the webhook in Stripe, copy the signing secret that Stripe displays and add it as your STRIPE_WEBHOOK_SECRET environment variable. Restart your backend to apply the change.
Need Help Configuring Stripe for Your Medusa.js Store?
Part 2: Setting Up Razorpay in Medusa.js v2
Part 2: Setting Up Razorpay in Medusa.js v2
Step 1: Create a Razorpay Account and Generate API Keys
Register for a Razorpay account at razorpay.com. Once your account is verified, navigate to Settings, then API Keys in the Razorpay dashboard. Generate a new key pair. You will receive a Key ID and a Key Secret. Keep the Key Secret secure and never expose it in client-side code.
Also note your Razorpay Account Number or Merchant ID, which you will need for the plugin configuration. Enable the webhook secret from the Razorpay dashboard under Webhooks settings, and note this value separately.
Step 2: Install the Razorpay Plugin
Unlike Stripe, Razorpay is not bundled with Medusa.js by default. The community-maintained plugin @tsc_tech/medusa-plugin-razorpay-payment supports Medusa.js v2 and is actively maintained. Install it in your backend:
npm install @tsc_tech/medusa-plugin-razorpay-paymentThis plugin is specifically designed for Indian merchants and supports UPI, NetBanking, Wallets, and card payments through Razorpay's standard checkout flow.
Step 3: Configure the Razorpay Provider in medusa-config.ts
Register the Razorpay plugin as a payment provider alongside Stripe in your medusa-config.ts providers array:
providers: [
// Stripe provider (already configured above)
{
resolve: "@tsc_tech/medusa-plugin-razorpay-payment/providers/razorpay",
id: "razorpay",
options: {
key_id: process.env.RAZORPAY_ID,
key_secret: process.env.RAZORPAY_SECRET,
razorpay_account: process.env.RAZORPAY_ACCOUNT,
webhook_secret: process.env.RAZORPAY_WEBHOOK_SECRET,
automatic_expiry_period: 30,
refund_speed: "normal",
},
},
],Step 4: Set Razorpay Environment Variables
Add the following to your backend .env file:
RAZORPAY_ID=rzp_test_your_key_id_here
RAZORPAY_SECRET=your_razorpay_key_secret_here
RAZORPAY_ACCOUNT=your_merchant_id_here
RAZORPAY_WEBHOOK_SECRET=your_webhook_secret_hereFor your Next.js storefront, add the public Razorpay key:
NEXT_PUBLIC_RAZORPAY_KEY=rzp_test_your_key_id_hereStep 5: Enable Razorpay for the India Region
Restart your Medusa backend. In the Medusa Admin dashboard, go to Settings, then Regions, and select your India region. Add Razorpay as a payment provider and save. Customers whose storefront request is scoped to the India region will now see Razorpay as a checkout option.
Step 6: Configure Razorpay Webhooks
In your Razorpay dashboard, navigate to Settings, then Webhooks, and add a new webhook endpoint. The URL follows the same Medusa pattern: your backend URL followed by /hooks/payment/razorpay_razorpay. Select the following events:
payment.authorized
payment.captured
payment.failed
refund.created
order.paid
Enter your webhook secret in the Razorpay webhook configuration form. This secret must match the RAZORPAY_WEBHOOK_SECRET value in your backend .env file.
Setting Up Razorpay for an Indian Ecommerce Store on Medusa.js?
Adding the Payment UI in Your Next.js Storefront
Adding the Payment UI in Your Next.js Storefront
Stripe Payment Element
Medusa's Next.js Starter Storefront includes a pre-built Stripe integration using Stripe's Payment Element component. The Payment Element renders a single UI widget that supports all Stripe payment methods available in the customer's region including card, Google Pay, Apple Pay, and local methods, without requiring separate UI code for each payment type.
To initialize the Stripe payment session in your checkout component, call the initiatePaymentSession function with the provider_id set to pp_stripe_stripe. This ID follows Medusa's convention of prefixing provider identifiers with pp_ followed by the provider name and ID you set in medusa-config.ts. The full guide for customizing the Stripe integration in the Next.js Starter is at docs.medusajs.com/resources/nextjs-starter/guides/customize-stripe.
Razorpay Payment Button
Razorpay's checkout uses a different pattern from Stripe. Rather than rendering a self-contained form widget, Razorpay opens a hosted payment modal when the customer clicks the pay button. Your storefront triggers this modal by calling the Razorpay JavaScript SDK with the order ID returned from the Medusa payment session.
Install the react-razorpay package in your Next.js storefront:
npm install react-razorpayIn your payment button component, use the useRazorpay hook from react-razorpay to initialize the Razorpay instance. When the customer clicks pay, construct the options object using the cart data and the session data returned by Medusa, then call new Razorpay(options).open() to display the payment modal. On successful payment, call placeOrder to complete the Medusa cart and create the order.
Running Stripe and Razorpay Together
Running both providers simultaneously requires your storefront to detect which provider is active for the current session and render the appropriate payment UI. Medusa returns the available payment sessions for a cart via the cart's payment_sessions array. You check each session's provider_id to determine whether to render the Stripe Payment Element or the Razorpay button.
The simplest approach is a conditional render in your payment step component:
npm install react-razorpayIn your payment button component, use the useRazorpay hook from react-razorpay to initialize the Razorpay instance. When the customer clicks pay, construct the options object using the cart data and the session data returned by Medusa, then call new Razorpay(options).open() to display the payment modal. On successful payment, call placeOrder to complete the Medusa cart and create the order.
Running Stripe and Razorpay Together
Running Stripe and Razorpay Together
Running both providers simultaneously requires your storefront to detect which provider is active for the current session and render the appropriate payment UI. Medusa returns the available payment sessions for a cart via the cart's payment_sessions array. You check each session's provider_id to determine whether to render the Stripe Payment Element or the Razorpay button.
The simplest approach is a conditional render in your payment step component:
const activeSession = cart.payment_collection?.payment_sessions?.find(
(s) => s.status === "pending"
);
if (activeSession?.provider_id === "pp_stripe_stripe") {
return <StripePaymentElement session={activeSession} />;
}
if (activeSession?.provider_id === "pp_razorpay_razorpay") {
return <RazorpayPaymentButton session={activeSession} cart={cart} />;
}This pattern keeps each provider's UI logic cleanly isolated. As you add more payment providers in the future, you extend this switch without restructuring the existing provider components.
Common Configuration Errors and How to Fix Them
Common Configuration Errors and How to Fix Them
The payment provider not appearing in checkout is almost always caused by one of two things: the provider was not enabled for the correct region in the Medusa Admin, or the backend was not restarted after updating medusa-config.ts. Check both before investigating further.
A webhook verification failure means either the webhook secret is wrong or the request body is being modified before Medusa reads it. If you have a reverse proxy like Nginx in front of your Medusa backend, ensure it is forwarding the raw request body without modification. Stripe and Razorpay both verify the signature against the exact byte sequence of the incoming request, so any transformation of the body will cause signature verification to fail.
For Razorpay specifically, the Razorpay modal may fail to open if the NEXT_PUBLIC_RAZORPAY_KEY environment variable is missing from your storefront's .env.local file. This key must be the public Key ID, not the secret. The modal also requires the phone number field to be populated in the billing address. If your checkout flow does not collect phone number, add that field before the payment step to avoid silent failures in the Razorpay flow.
Going Live: Switching from Test to Production Keys
Going Live: Switching from Test to Production Keys
When you are ready to accept real payments, swap the test API keys in your environment variables for live keys. In Stripe, navigate to Developers, then API Keys, and toggle off Test Mode to see your live keys. In Razorpay, switch from Test Mode to Live Mode in the dashboard header to access your live Key ID and Secret.
Update your webhook endpoints in both dashboards to point to your production backend URL. Delete any test webhook endpoints to avoid duplicate event processing. Re-test with a real card transaction for a small amount to confirm the end-to-end flow works in production before announcing the store to customers.
For teams deploying Medusa.js on AWS or another cloud provider, the Askan Ecomm article on Medusa.js performance optimization covers the infrastructure setup that ensures your payment webhook handlers respond reliably under load. Payment webhooks that time out due to server resource constraints are one of the more frustrating causes of incomplete order processing in production stores.
Ready to Launch Your Medusa.js Store with Stripe and Razorpay?
Payment integration in Medusa.js is methodical rather than mysterious. The platform's provider-based architecture means Stripe and Razorpay both plug into the same Payment Module and behave consistently from the admin and fulfillment perspective. The differences are in the frontend UI patterns each gateway requires, the webhook event names you register, and the regional configuration that determines which provider a customer sees at checkout.
With both providers configured and tested, your store can serve Indian customers through Razorpay's extensive local payment method support and international customers through Stripe's globally trusted card and wallet infrastructure, all from a single Medusa.js backend without any custom payment routing middleware between them.
Written by
Manikandan Arumugam
CDO
