PayU India payment gateway integration for Medusa v2 with redirect flow.
A seamless PayU India payment integration for MedusaJS v2.
This plugin enables a redirect-based checkout flow with PayU, complete with robust webhook handling to automatically verify and capture transactions even if the user drops off during the redirect.
yarn add @sam-ael/medusa-plugin-payu
Or with npm:
npm install @sam-ael/medusa-plugin-payu
Add your PayU credentials to your file:
# RequiredPAYU_MERCHANT_KEY="your_merchant_key"PAYU_MERCHANT_SALT="your_merchant_salt" # Note: This plugin uses Salt V1 for hashing!PAYU_ENVIRONMENT="test" # or "production"# Optional Base URLs (used to build the redirect URLs dynamically based on context)STOREFRONT_URL="http://localhost:8000"# PAYU_REDIRECT_URL="/order/confirmed"# PAYU_REDIRECT_FAILURE_URL="/checkout"
import { defineConfig } from "@medusajs/framework/utils"export default defineConfig({modules: [{resolve: "@medusajs/medusa/payment",options: {providers: [{resolve: "@sam-ael/medusa-plugin-payu/providers/payu",id: "payu",options: {merchantKey: process.env.PAYU_MERCHANT_KEY,merchantSalt: process.env.PAYU_MERCHANT_SALT,environment: process.env.PAYU_ENVIRONMENT || "test",},},],},},],})
Make sure you go into your Medusa Admin β Settings β Regions and add as a payment provider to your Indian region.
PayU requires a redirect-based flow. Because MedusaJS's doesn't directly redirect the user natively, the plugin generates the required raw signature hashes and returns them in the attribute of the payment session.
You are expected to construct a hidden HTML form using this data and automatically submit it on the frontend to execute the redirect.
Note: PayU requires , , and . The plugin automatically attempts to extract this from the Medusa cart's shipping/billing address. To avoid errors, you can explicitly pass in the data payload when initializing the payment session.
"use client"function PayUPaymentButton({ cart }) {const handlePayment = async () => {const paymentSession = cart.payment_collection?.payment_sessions?.find((session) => session.provider_id === "pp_payu_payu")if (!paymentSession?.data?.form_data) return;const { form_data, paymentUrl } = paymentSession.data// Create a hidden form to POST to PayUconst form = document.createElement("form")form.method = "POST"form.action = paymentUrlObject.entries(form_data).forEach(([key, value]) => {const input = document.createElement("input")input.type = "hidden"input.name = keyinput.value = String(value)form.appendChild(input)})document.body.appendChild(form)form.submit()}return <button onClick={handlePayment}>Pay with PayU</button>}
Users often close the browser before completing the redirect back to the store. Setting up Server-to-Server callbacks ensures Medusa captures the payment regardless.
The plugin handles reverse SHA-512 verification automatically to ensure incoming webhooks are strictly from PayU and have not been tampered with.
When using the environment, use PayU's standard test cards (e.g., , CVV: , any future expiry).
Note that for webhook testing locally, you will need a tunneling service like ngrok to expose your local Medusa instance to PayU's webhook dispatcher.
MIT