npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

medusa-payment-provider

v0.0.9

Published

Universal payment provider plugin for Medusa v2 — Razorpay, Stripe, PayPal, PayU, Juspay, PhonePe, Paytm, Cashfree, HDFC, PagLocal (PayGlocal), plus 1-click UPI identifiers.

Readme

medusa-payment-provider

Universal payment provider plugin for Medusa v2. Supports 10 gateways plus 1-click UPI identifiers for Indian methods:

| Provider | Identifier | Flow | |----------|------------|------| | Cashfree | cashfree | Server-side order + JS SDK | | Cashfree UPI | cashfree_upi | Order Pay UPI link (intent) | | HDFC SmartGateway | hdfc | Payment page session | | HDFC UPI | hdfc_upi | UPI-only payment page session | | Razorpay | razorpay | Orders API + Checkout JS | | Razorpay UPI | razorpay_upi | UPI QR Codes API (inline QR / upi://) | | Stripe | stripe | PaymentIntent + Elements | | PayPal | paypal | Checkout order + capture | | PayU | payu | Redirect + SHA512 hash | | PayU UPI | payu_upi | Redirect with pg=UPI / bankcode=INTENT | | Juspay | juspay | Smart-router payment page | | Juspay UPI | juspay_upi | UPI-only payment page session | | PhonePe | phonepe | Redirect + checksum (PAY_PAGE) | | PhonePe UPI | phonepe_upi | UPI_INTENT deep link | | Paytm | paytm | txnToken + payment page | | Paytm UPI | paytm_upi | UPI UPIPUSH (intent) | | PagLocal (PayGlocal) | paglocal | PayCollect GPI redirect |

*_upi identifiers are separate Medusa payment providers so checkout can show 1-click UPI next to the hosted-page option. They reuse the same gateway credentials as the parent provider. No extra environment variables.

Installation

npm install medusa-payment-provider

Copy .env.example to your host .env and fill provider credentials.

Registration

All providers (unified)

// medusa-config.ts
{
  resolve: "medusa-payment-provider",
  options: {
  },
}

The unified package registers hosted-page and *_upi services. Enable the ones you want on the INR region in Admin (pp_phonepe_upi, pp_razorpay_upi, …).

Single provider (selective)

{
  resolve: "medusa-payment-provider/providers/razorpay",
  id: "razorpay",
  options: {
    key_id: process.env.RAZORPAY_KEY_ID,
    key_secret: process.env.RAZORPAY_KEY_SECRET,
    webhook_secret: process.env.RAZORPAY_WEBHOOK_SECRET,
  },
}

Repeat for stripe, paypal, payu, juspay, phonepe, paytm, cashfree, hdfc, paglocal.

1-click UPI (same options as the parent gateway):

{
  resolve: "medusa-payment-provider/providers/phonepe-upi",
  id: "phonepe_upi",
  options: {
    merchant_id: process.env.PHONEPE_MERCHANT_ID,
    salt_key: process.env.PHONEPE_SALT_KEY,
    salt_index: process.env.PHONEPE_SALT_INDEX,
    environment:
      process.env.PHONEPE_ENVIRONMENT === "production" ? "production" : "sandbox",
  },
}

Also: razorpay-upi, cashfree-upi, payu-upi, paytm-upi, juspay-upi, hdfc-upi.

PagLocal (PayGlocal PayCollect)

{
  resolve: "medusa-payment-provider/providers/paglocal",
  id: "paglocal",
  options: {
    merchant_id: process.env.PAGLOCAL_MERCHANT_ID,
    private_key: process.env.PAGLOCAL_PRIVATE_KEY?.replace(/\\n/g, "\n"),
    key_id: process.env.PAGLOCAL_KEY_ID,
    webhook_merchant_id: process.env.PAGLOCAL_MERCHANT_ID,
    environment:
      process.env.PAGLOCAL_ENVIRONMENT === "production" ? "production" : "sandbox",
  },
}

Configure the PayGlocal webhook URL with PayGlocal support: https://<host>/webhooks/paglocal.

1-click UPI (storefront contract)

UPI Intent is mobile-first. Desktop checkouts should show a QR or the HTTPS payment_url fallback.

Every *_upi initiate writes:

{
  flow: "upi_intent",
  upi_intent_url?: string, // upi:// or app deep link when the gateway returns one
  payment_url?: string,    // HTTPS fallback / hosted form / Checkout JS
  // plus existing gateway ids (order_id, merchant_transaction_id, ...)
}

Storefront:

  1. Enable pp_{id}_upi on the region (e.g. pp_phonepe_upi).
  2. If session.data.flow === "upi_intent" and upi_intent_url is set, navigate there on mobile (window.location.href = upi_intent_url).
  3. Otherwise use payment_url (or PayU form fields / Razorpay Checkout JS with UPI preselected).
  4. After return, call existing authorizePayment; webhooks still complete the session.

PayU UPI has no upi:// URL from the server: auto-post the hosted form (pg=UPI, bankcode=INTENT). Hash is unchanged (pg / bankcode are not hashed).

Webhooks

Plugin routes (recommended for signature verification)

Configure your gateway to POST to:

| Provider | URL | |----------|-----| | Razorpay | https://<host>/webhooks/razorpay | | Razorpay UPI | https://<host>/webhooks/razorpay_upi | | Stripe | https://<host>/webhooks/stripe | | PayPal | https://<host>/webhooks/paypal | | PayU | https://<host>/webhooks/payu | | PayU UPI | https://<host>/webhooks/payu_upi | | Juspay | https://<host>/webhooks/juspay | | Juspay UPI | https://<host>/webhooks/juspay_upi | | PhonePe | https://<host>/webhooks/phonepe | | PhonePe UPI | https://<host>/webhooks/phonepe_upi | | Paytm | https://<host>/webhooks/paytm | | Paytm UPI | https://<host>/webhooks/paytm_upi | | Cashfree | https://<host>/webhooks/cashfree | | Cashfree UPI | https://<host>/webhooks/cashfree_upi | | HDFC | https://<host>/webhooks/hdfc | | HDFC UPI | https://<host>/webhooks/hdfc_upi | | PagLocal (PayGlocal) | https://<host>/webhooks/paglocal |

Routes preserve req.rawBody for HMAC verification and always respond 200 (gateways retry on failure).

Gateways typically allow one dashboard webhook URL:

  • UPI-only region (parent identifier not registered): point the gateway at /webhooks/{id}_upi.
  • Hosted + UPI both enabled: keep /webhooks/{id}. getWebhookActionAndData is inherited; Medusa then authorizes by session_id using the session’s own provider id.

Medusa built-in hook

Medusa also exposes /hooks/payment/{provider} which emits a delayed event. Plugin routes call IPaymentModuleService.getWebhookActionAndData() directly.

Amount handling

Medusa v2 passes payment amounts in major currency units (e.g. 100.50 = ₹100.50), not paise/cents.

| Providers | Medusa | Gateway API | |-----------|--------|-------------| | Cashfree, PayPal, PayU, Paytm, Juspay, PagLocal, HDFC | major | major (decimal string or number) | | Stripe, Razorpay, PhonePe | major | minor (smallest unit; converted via currency exponent) |

Currency exponents: 0 (JPY, KRW, VND, …), 3 (BHD, JOD, KWD, OMR, TND), 2 (default, including unknown codes). Conversion uses string/integer scaling with round half-up — never native float × 100.

Session data writes amount plus amount_unit: "major". Unmarked legacy session amounts are not trusted for money decisions (prefer gateway-converted or current Medusa amount). Abandon/recreate in-flight checkouts created before this fix.

Known issue (ops): if this plugin ran in production with the previous wrong unit assumption, some charges may have been ~100× too low. Finance should reconcile gateway settlements vs order totals for that window — no automatic re-charge in this plugin.

Staging checklist before prod: one charge + full refund + partial refund + webhook per family (major-unit vs minor-unit providers). Initiate logs include medusa_amount, gateway_amount, amount_unit for anomaly grepping. For UPI, also complete one mobile intent payment + desktop fallback per enabled *_upi provider.

Order cancel / refund (Razorpay)

  • Refund: Resolves payment_id from session data or Razorpay order payments. Already-refunded / fully-refunded gateway states are treated as success so Medusa cancelOrderWorkflow can finish. Missing gateway payment (never paid) is a no-op success.
  • Cancel (uncaptured): Attempts to void authorized payments via full refund when Razorpay reports authorized; otherwise marks the session canceled locally without throwing.

Development

cd medusa-payment-provider
npm install
./node_modules/.bin/medusa plugin:build
npm test

Compatibility

Medusa >= 2.4.0 (tested with @medusajs/[email protected]).