Commerce

How to Build a Razorpay Payment Provider for Medusa v2 (UPI, Cards, Netbanking)

Medusa v2 has no official Razorpay plugin. This guide walks through building a custom payment provider module for Indian payment methods — UPI, cards, netbanking — with webhook signature verification and the common pitfalls that break checkout.

MManojAugust 4, 202612 min read
#medusa#commerce#razorpay#payments
Share

You have picked Medusa v2 for your Indian e-commerce store. You open the plugin registry and search for Razorpay. Nothing official exists. The v1 plugin was never ported — GitHub #9881 confirms it. A handful of community packages (@devx-commerce/razorpay, medusa-plugin-razorpay-v2, @sgftech/payment-razorpay) exist on npm, each with sparse documentation and version-pinning issues. This guide walks through building your own Razorpay payment provider module from scratch — UPI, cards, netbanking — using Medusa v2's AbstractPaymentProvider, with the webhook signature verification fix that trips up everyone (GitHub #9541), and an interactive flow debugger so you can trace exactly where each payment method breaks.

Setting up the rest of the India stack?

Once payments work, you will need cash on delivery + Shiprocket fulfillment for the 60%+ of Indian orders that ship COD. And if you are self-hosting Medusa, the Coolify + Hetzner deployment guide covers the production-hardened Docker setup.

The v1 Razorpay plugin broke on upgrade because Medusa v2 replaced the plugin loader with a module system — PaymentProcessor became AbstractPaymentProvider, and the entire checkout workflow was rewritten. If you are porting an existing v1 integration, almost nothing carries over except the Razorpay API calls themselves. I am Manoj, commerce and ERP implementation lead at Mith Tech in Bengaluru, and wiring Indian payment rails into headless commerce stacks is the work we do every week.

Why there is no official Razorpay plugin

Medusa v2 shipped in late 2024 with a completely rewritten module system. The v1 plugin architecture — where payment plugins implemented PaymentProcessor and were loaded from medusa-config.js — was replaced by a module-based system where providers extend AbstractPaymentProvider and register through medusa-config.ts.

The original community Razorpay plugin (SGFGOV/medusa-payment-razorpay) was built for v1. When developers upgraded to v2, the plugin stopped loading entirely — the error was Unable to find the plugin 'medusa-payment-razorpay' (GitHub #9881). The maintainer has since published a v2-compatible package (@sgftech/payment-razorpay), and other community alternatives exist (@devx-commerce/razorpay, @tsc_tech/medusa-plugin-razorpay-payment, medusa-plugin-razorpay-v2), but none have official backing from the Medusa team.

This matters because Medusa's payment flow has specific requirements around session management, webhook handling, and the completeCartWorkflow that community plugins handle inconsistently. Building your own module gives you control over the parts that break — and they will break, particularly around webhook signature verification.

The payment flow — step by step

Every Razorpay payment through Medusa follows the same six-stage sequence, regardless of method. The specifics differ for UPI (async approval), cards (3DS redirect), and netbanking (bank redirect), but the Medusa side is identical.

InteractivePayment Flow Debuggerlink

Select a payment method and click through each step to see the exact API calls between your storefront, Medusa backend, and Razorpay — with the known failure points flagged.

The critical insight is that UPI and netbanking are asynchronous — the customer approves on a different device or in a bank redirect, and the result comes back via webhook. Medusa's default payment flow assumes synchronous card-style auth (GitHub #13837), which is why async methods fail if your webhook handler is not wired correctly.

The webhook signature problem

This is the single most common failure point for Razorpay integrations in Medusa v2, and it fails silently — your webhook returns 401, Razorpay retries a few times, gives up, and the customer's payment is captured but their cart never becomes an order.

The root cause: Razorpay's webhook signature verification requires the raw request body as a string. Medusa v2's API route layer parses the body as JSON before your handler runs (GitHub #9541, 39 comments). By the time your handler calls validateWebhookSignature(), the body has been parsed and re-serialised, which changes whitespace and key ordering — and the HMAC no longer matches.

The workaround: use JSON.stringify(req.body) for the raw body comparison. This works because Razorpay's webhook payloads use consistent JSON formatting. For a stricter approach, you can use a custom middleware to capture the raw body before parsing — but the JSON.stringify approach is what most production implementations use.

Double-capture risk

If you use both webhooks and the storefront handler callback to capture payments, you can accidentally capture twice (GitHub #13301, 94 comments — the highest-comment Stripe issue in the repo, but the same pattern applies to Razorpay). The fix: use an idempotency check in your capturePayment method. Query the Razorpay order status before calling capture — if it is already paid, return the existing payment rather than calling the capture API again.

Validating your configuration

Before writing any code, check your Razorpay credentials and module configuration. The three most common setup errors are: wrong key format (GitHub #1841), missing webhook secret, and sending amounts in rupees instead of paise.

CalculatorRazorpay Config Validatorlink

Enter your Razorpay credentials (nothing is sent anywhere — validation runs entirely in your browser) to check for the five most common configuration errors.

Which payment methods to enable

India's payment landscape is not like the West. UPI accounts for over 60% of online transactions. Cards are secondary. Netbanking is the fallback for high-value B2B orders. Wallets (Paytm, PhonePe) are declining as UPI absorbs their use cases, but still matter for specific demographics.

The right mix depends on your business model:

InteractivePayment Method Deciderlink

Select your business model to get a recommended payment method mix, Razorpay configuration, and the edge cases to watch for.

Select your business model:

UPI-specific considerations

UPI has a transaction limit of ₹1,00,000 for most banks (some allow up to ₹5,00,000 for merchant payments). Your checkout should validate the cart total against this limit before opening the UPI flow — otherwise the customer goes through the entire approval process on their phone only to get a rejection from their bank.

The Razorpay Checkout modal handles QR code display and VPA input automatically. For a better conversion rate, pre-fill the prefill.contact field with the customer's phone number from the shipping address — Razorpay can auto-detect linked UPI handles.

RBI tokenisation mandate

Since October 2022, the RBI requires that no payment aggregator or merchant store actual card numbers. Razorpay handles this through their token vault — when a customer saves a card, Razorpay stores a token and you reference that token for future payments. This is transparent if you use Razorpay Checkout (the modal handles it), but if you build a custom card form, you must use Razorpay's tokenisation API. Storing raw PANs is a compliance violation.

Building the payment provider module

The code recipe below covers four files: the payment provider service, the webhook handler, the storefront checkout component, and the module registration in medusa-config.ts.

Code recipeRazorpay Code Recipelink

Four-tab code recipe — payment provider module, webhook handler with signature verification, Next.js storefront checkout button, and medusa-config.ts registration.

// src/modules/razorpay/service.ts
import {
  AbstractPaymentProvider,
  PaymentProviderError,
  PaymentSessionStatus,
} from "@medusajs/framework/utils"
import Razorpay from "razorpay"

type RazorpayOptions = {
  key_id: string
  key_secret: string
  webhook_secret: string
}

class RazorpayPaymentProvider extends AbstractPaymentProvider<
  RazorpayOptions
> {
  static identifier = "razorpay"
  protected client_: InstanceType<typeof Razorpay>
  protected options_: RazorpayOptions

  constructor(container: Record<string, unknown>, options: RazorpayOptions) {
    super(container, options)
    this.options_ = options
    this.client_ = new Razorpay({
      key_id: options.key_id,
      key_secret: options.key_secret,
    })
  }

  async initiatePayment(input: {
    amount: number
    currency_code: string
    context: Record<string, unknown>
  }) {
    const order = await this.client_.orders.create({
      amount: input.amount, // already in paise
      currency: input.currency_code.toUpperCase(),
      receipt: input.context.cart_id as string,
    })

    return {
      id: order.id,        // razorpay_order_id
      data: { order_id: order.id },
    }
  }

  async getPaymentStatus(
    paymentSessionData: Record<string, unknown>
  ): Promise<PaymentSessionStatus> {
    const orderId = paymentSessionData.order_id as string
    const order = await this.client_.orders.fetch(orderId)

    switch (order.status) {
      case "paid":
        return PaymentSessionStatus.AUTHORIZED
      case "attempted":
        return PaymentSessionStatus.PENDING
      default:
        return PaymentSessionStatus.PENDING
    }
  }

  // ... capturePayment, refundPayment, cancelPayment
}

export default RazorpayPaymentProvider

Scaffold the module directory

Create src/modules/razorpay/ with service.ts and index.ts. The service extends AbstractPaymentProvider from @medusajs/framework/utils. Install the Razorpay Node SDK: npm install razorpay.

Implement the five required methods

initiatePayment creates a Razorpay order and returns the order_id in the session data. getPaymentStatus fetches the order and maps Razorpay's paid/attempted/created states to Medusa's PaymentSessionStatus. capturePayment calls POST /v1/payments/:id/capture. refundPayment calls POST /v1/payments/:id/refund. cancelPayment is a no-op for Razorpay (orders expire automatically after 30 minutes).

Register in medusa-config.ts

Add the module to the modules array with key_id, key_secret, and webhook_secret from environment variables. Never hard-code credentials.

Build the webhook route

Create src/api/hooks/razorpay/route.ts. Verify the signature, resolve the payment session by order_id, mark it as authorised, and trigger completeCartWorkflow. Handle payment.authorized, payment.captured, and payment.failed events.

Wire the storefront

Load Razorpay Checkout.js via a <Script> tag. On checkout, create a payment session via the Medusa Store API, then open the Razorpay modal with the order_id from the session. The handler callback fires on successful payment — use it to complete the cart.

Test with Razorpay test credentials

Use success@razorpay as the UPI VPA for successful UPI payments. Use card 4111 1111 1111 1111 with any future expiry and CVV 111 for card payments. Verify that the webhook fires, the payment session transitions to authorized, and the cart becomes an order.

What to know before you commit

Vendor lock-in. Razorpay is the payment layer — switching to Cashfree, PayU, or Juspay later means rewriting the payment provider module. The module interface is standardised (AbstractPaymentProvider), so the Medusa side stays the same, but the API calls, webhook formats, and error codes all change.

Settlement cycle. Razorpay settles to your bank account on T+2 for most payment methods. UPI and netbanking settlements can be T+1 with Razorpay's instant settlement feature (additional fee). Factor this into your cash-flow planning.

Refund processing. UPI refunds take 5–7 business days. Card refunds take 5–10 business days. Netbanking refunds can take up to 14 business days. Your order management workflow needs to handle the refund-pending state — Medusa marks the refund as processed immediately, but the customer does not see the money for days.

GST on Razorpay fees. Razorpay charges 2% + GST on card/netbanking transactions. UPI transactions are currently zero-MDR for merchants (as per RBI mandate), though Razorpay charges a platform fee on some plans. Your reconciliation must account for the 18% GST on payment gateway fees.

PCI-DSS compliance. If you use Razorpay Checkout (the hosted modal), Razorpay handles PCI-DSS compliance — card data never touches your server. If you build a custom card form and use the Razorpay.js tokenisation API, you are in SAQ A-EP scope. Most Indian D2C brands should use the hosted checkout.

+How do I add Razorpay to Medusa v2?

Create a custom payment provider module that extends AbstractPaymentProvider from @medusajs/framework/utils. Implement initiatePayment (creates a Razorpay order), getPaymentStatus (maps order status), capturePayment (captures funds), and refundPayment. Register the module in medusa-config.ts. There is no official first-party Razorpay plugin for v2.

+Why does Razorpay webhook signature verification fail in Medusa v2?

Medusa v2's API route layer parses the request body as JSON before your handler runs. Razorpay's HMAC-SHA256 signature is computed over the raw body string. By the time your handler sees it, the body has been parsed and re-serialised, changing whitespace and key ordering. Use JSON.stringify(req.body) as the raw body for verification, or add a custom middleware to capture the raw body before parsing. This is documented in GitHub #9541.

+What payment methods should I enable for an Indian Medusa store?

Enable UPI (60%+ of Indian online transactions), cards (Visa/Mastercard/RuPay), and netbanking (SBI/HDFC/ICICI at minimum). UPI is the primary method — optimise your checkout to show the QR code prominently. For B2B, add netbanking with no upper transaction limit. Wallets (Paytm/PhonePe) are declining but still relevant for some demographics.

+Can I use the old v1 Razorpay plugin with Medusa v2?

No. The v1 plugin (medusa-payment-razorpay) uses the PaymentProcessor interface which was removed in v2. Attempting to use it produces Unable to find the plugin errors (GitHub #9881). You need either a community v2-compatible package (@sgftech/payment-razorpay, @devx-commerce/razorpay) or a custom module as described in this guide.

+What is the UPI transaction limit for Razorpay payments?

Most banks cap UPI merchant payments at ₹1,00,000 per transaction. Some banks (like HDFC and ICICI) allow up to ₹5,00,000 for specific merchant categories. Your checkout should validate the cart total against this limit before opening the UPI flow. For higher amounts, redirect to netbanking or use Razorpay Payment Links.

+Does Razorpay charge fees on UPI transactions?

UPI transactions are currently zero-MDR for merchants as per the RBI mandate — no interchange or processing fee. However, Razorpay may charge a platform fee on some subscription plans. Card and netbanking transactions attract 2% + 18% GST. Check Razorpay's current pricing page for your plan.

+How do I handle refunds in a Medusa + Razorpay setup?

Call POST /v1/payments/:id/refund with the amount in paise and a speed parameter (normal or optimum). UPI refunds take 5–7 business days, card refunds 5–10, netbanking up to 14. Medusa marks the refund as processed immediately but the customer sees the credit later. Track refund status via the refund.processed webhook event.

+What version of Medusa does this guide apply to?

This guide targets Medusa v2.18.0 (the latest stable release as of August 2026). The AbstractPaymentProvider interface has been stable since v2.0 — the module structure and registration work the same way across v2.x releases. The webhook handling approach may need adjustment if Medusa changes its API route body parsing in a future release.

Razorpay's payment rails work — the hard part is wiring them through Medusa's module system without the silent failures that eat orders. The flow debugger and config validator above catch the five issues that account for most integration failures. If your test webhook returns 200 and your cart becomes an order, the production setup is the same code with live keys and a verified domain.

Building an Indian e-commerce store on Medusa?

We wire payment gateways, fulfilment, and GST compliance into headless commerce stacks for Indian businesses. If you are stuck on the Razorpay integration or need the full India stack (UPI + COD + Shiprocket), we will scope it honestly.

Free · By email

Get practical ERPNext & automation guides

New implementation guides, cost breakdowns and open-source tips for Indian businesses — occasionally, straight to your inbox. No spam.

M

Written by

Manoj

Founder of Mith Tech, an open-source ERP & automation studio. Hands-on ERPNext/Frappe implementation across multi-branch, multi-warehouse Indian operations — GST/TDS/PT compliance, branch-level permissions, and custom Frappe apps that give management real-time visibility.

Keep reading

See what this looks like for your business

Book a free 30-minute audit. We'll map your workflows, find where time and money leak, and design an open-source stack you actually own — no per-user licence fees.

Book a consultation
0 0
Published on 4 August 2026

Manoj

Comments

No comments yet. Start a new discussion.

Ctrl+Enter to add comment