PayU India payment gateway plugin for MedusaJS 2.x with redirect-based checkout, webhook support, hash verification, and refunds.
PayU India payment gateway plugin for MedusaJS 2.x with redirect-based checkout flow.
npm install medusa-payu-payment-plugin# oryarn add medusa-payu-payment-plugin
Add to your file:
# PayU CredentialsPAYU_MERCHANT_KEY=your_merchant_keyPAYU_MERCHANT_SALT=your_merchant_saltPAYU_ENVIRONMENT=test # or "production"# Redirect URLs (Optional - defaults provided)STOREFRONT_URL=http://localhost:8000 # or use NEXT_PUBLIC_BASE_URL# PAYU_REDIRECT_URL=/order/confirmed # Default: /order/confirmed# PAYU_REDIRECT_FAILURE_URL=/checkout # Default: /checkout
Add to your :
import { defineConfig } from "@medusajs/framework/utils"export default defineConfig({// ... other configmodules: [{resolve: "@medusajs/medusa/payment",options: {providers: [{resolve: "medusa-payu-payment-plugin/providers/payu",id: "payu",options: {merchantKey: process.env.PAYU_MERCHANT_KEY,merchantSalt: process.env.PAYU_MERCHANT_SALT,environment: process.env.PAYU_ENVIRONMENT || "test",},},],},},],})
In Medusa Admin:
When creating a payment session, the following customer data is required:
The phone number fallback uses MedusaJS's which provides the customer and billing address data. If the billing address phone is not available, pass the shipping address phone when initiating payment.
It is also highly recommended to pass and so they persist through to the webhook even if the session is lost.
// When creating payment session, include in data:{shipping_address_phone: cart.shipping_address?.phone,cart_id: cart.id, // Mapped to udf1customer_id: customer.id, // Mapped to udf2country_code: "in" // For URL construction}
"use client"function PayUPaymentButton({ cart }) {const handlePayment = async () => {// Get PayU payment sessionconst paymentSession = cart.payment_collection?.payment_sessions?.find((session) => session.provider_id === "pp_payu_payu")if (!paymentSession?.data?.form_data) {console.error("PayU session not found")return}const { form_data, paymentUrl } = paymentSession.data// Create and submit hidden formconst 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 (<buttononClick={handlePayment}className="btn-primary">Pay with PayU</button>)}
The payment session data contains:
{txnid: string // Unique transaction IDamount: string // Amount with 2 decimals (e.g., "999.00")productinfo: string // Product/order descriptionfirstname: string // Customer first nameemail: string // Customer emailphone: string // Customer phonehash: string // Security hash (SHA-512)paymentUrl: string // PayU checkout URLstatus: string // Payment statusform_data: { // Ready-to-submit form datakey: string // Merchant keytxnid: stringamount: stringproductinfo: stringfirstname: stringemail: stringphone: stringsurl: string // Success redirect URLfurl: string // Failure redirect URLhash: stringservice_provider: stringudf1: string // cart_idudf2: string // customer_id}}
PayU webhooks (S2S callbacks) ensure reliable payment status updates even when browser redirects fail.
Select events to subscribe:
Save the configuration
The plugin automatically handles security:
The webhook also logs (from udf1) and (from udf2) for easier debugging and reconciliation.
PayU sends webhooks as URL-encoded form data:
MedusaJS handles both content types automatically.
| Status | Action | Result |
|---|---|---|
| Payment session authorized, cart completed, order created | ||
| / | Payment session marked as failed | |
| Other | Logged for debugging, no action taken |
| Method | Description |
|---|---|
| Creates payment session with hash and form data | |
| Verifies payment status with PayU API | |
| Marks payment as captured (auto-capture enabled) | |
| Initiates full or partial refund | |
| Cancels pending payment | |
| Handles PayU webhook callbacks |
You can use the verify payment workflow in your custom code:
import { verifyPayuPaymentWorkflow } from "medusa-payu-payment-plugin/workflows"// In your API route or subscriberconst { result } = await verifyPayuPaymentWorkflow(container).run({input: {txnid: "TXN_1234567890_abcd",},})if (result.success) {console.log("Payment status:", result.status)console.log("Transaction details:", result.transaction)}
| Variable | Description | Required |
|---|---|---|
| PayU Merchant Key | Yes | |
| PayU Merchant Salt (Salt V1) | Yes | |
| or | No (default: ) | |
| or | Your storefront base URL (e.g., ) | Yes |
| Success redirect path (e.g., ) | No (default: ) | |
| Failure redirect path (e.g., ) | No (default: ) |
Use PayU test credentials in your test environment:
| Card Type | Number | CVV | Expiry |
|---|---|---|---|
| Visa | 4012001038443335 | 123 | Any future date |
| Mastercard | 5123456789012346 | 123 | Any future date |
Ensure:
Ensure:
Contributions are welcome! Please feel free to submit a Pull Request.
MIT © SAM-AEL
See LICENSE for more information.