Redsys / Sermepa TPV Virtual payment provider plugin for MedusaJS v2
Redsys / Sermepa TPV Virtual payment provider plugin for MedusaJS v2.
This plugin enables payment processing through Redsys' hosted payment page (TPV Virtual) via redirect flow. Customers are redirected to the Redsys secure payment page to complete their transaction.
Production-proven: This plugin is derived from a live production Medusa store processing real Redsys payments.
Add the following to your file:
For sandbox testing, use the following test credentials from Redsys:
In your :
Enable the Redsys provider(s) in your Medusa admin panel under Settings > Regions:
You can enable one or both providers depending on which payment methods you want to offer.
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
| string | Yes | — | Redsys HMAC-SHA256 secret key | |
| string | Yes | — | Redsys merchant code (FUC) | |
| string | No | Terminal number | ||
| string | No | or | ||
| string | No | — | Webhook URL for Redsys to POST transaction results | |
| string | No | — | URL to redirect after successful payment (URLOK) | |
| string | No | — | URL to redirect after failed payment (URLKO) | |
| string | No | = immediate capture, = pre-authorization |
A payment session is only authorized after a valid HMAC-confirmed webhook that fully matches the stored payment reference. returns for any session whose reference is missing, not confirmed, or mismatched (amount, currency, session, provider, merchant or transaction type). It never trusts the stored field, so a — or even a forged — status can never authorize a payment on its own.
The plugin generates a 12-character alphanumeric (e.g. ) used as Redsys' merchant order reference. When the cart is completed after payment, Medusa generates its own order ID (e.g. ). These are different IDs.
The callback URL from Redsys only contains the Redsys order ID, not the Medusa order ID. To bridge this gap, the storefront stores the mapping → in before redirecting to the TPV. The callback page uses the to retrieve/complete the order and redirect to the correct confirmation page.
Redsys is a redirect-based payment method (no card input in your storefront — the customer enters card data on Redsys' secure TPV). You must adapt your Medusa Next.js storefront with the changes below.
Security: starting with v1.1.1 the storefront must not call before redirecting to Redsys. Doing so would fail anyway (the payment is not authorized yet) and previously created unpaid orders. The Redsys webhook completes the cart after the payment is confirmed.
Add Redsys and Bizum to the payment info map and add helper functions:
The full copyable implementation lives in . The two critical changes vs. older versions:
If your storefront uses middleware to enforce region/country code prefixes in URLs (as the default Medusa Next.js storefront does), add a bypass so is not redirected. Add this early in the function:
Ensure your storefront domain is allowed in CORS:
The payment session field returned by :
Note: The identifier in the callback URL is the value returned by the library and is normal. This does not indicate a problem — the actual signature computation follows the Redsys v4.1 specification.
Medusa automatically exposes webhook endpoints for the Redsys providers at:
For local development with sandbox, you must expose your backend to the internet (e.g., via ngrok) so Redsys can reach the webhook. Set to the ngrok URL.
Important: Redsys sends the notification to but the signature verification and payment status update happens through the Medusa webhook handler — make sure points to the same endpoint or forward notifications accordingly.
The plugin creates and manages a small table to guarantee that only payments it initiated can ever be confirmed:
| Column | Purpose |
|---|---|
| Redsys order ID (primary key) | |
| Real Medusa payment session () | |
| or | |
| Medusa cart, if available | |
| , , | Expected amount/currency |
| , , | Expected merchant/terminal/type |
| , , | Set by the validated webhook |
The table is created lazily with on first use, so no manual migration is required. A webhook can only confirm a payment that the plugin itself recorded in /, and only if every field matches.
| Card Number | Brand | Behavior |
|---|---|---|
| 4548810000000003 | VISA | 3DS v2 approved |
| 5576441563045037 | Mastercard | 3DS v2 approved |
| 4548814479727229 | VISA | 3DS frictionless |
| 4548817212493017 | VISA | 3DS challenge |
| Any + CVV 999 | Any | Payment declined |
Important: In sandbox, Bizum transactions cannot exceed 10€. Use a discount coupon or low-price test product.
| Field | Value |
|---|---|
| Phone number | |
| PIN | |
| SMS code |
Test scenarios by amount:
| Amount | Result |
|---|---|
| < 5€ | Payment approved |
| 5€ - 10€ | Payment approved |
| 10€ - 15€ | Payment declined (exceeds sandbox limit) |
| > 15€ | Payment declined (no Bizum user) |
| Code | Type | Description |
|---|---|---|
| Payment | Authorization + immediate capture (default) | |
| Pre-authorization | Reserve funds only | |
| Confirmation | Capture pre-authorized funds | |
| Refund | Full or partial refund | |
| Cancellation | Cancel/void a transaction |
Sessions created before v1.1.1 do not carry a and have no payment reference, so they will not be authorized (fail-closed). Customers in the middle of a checkout will need to refresh / recreate their payment session. This is intentional: it is safer to reject than to authorize an unverified payment.
The plugin includes built-in numeric currency codes for all major currencies (see for the full list). Unsupported currencies are rejected with an error rather than silently falling back to EUR.
MIT — see LICENSE file for details.
For issues and questions, please open an issue on GitHub.
1npm install @jsm406/medusa-plugin-redsys2# or3yarn add @jsm406/medusa-plugin-redsys4# or5pnpm add @jsm406/medusa-plugin-redsys1REDSYS_SECRET_KEY=sq7Hj....2REDSYS_MERCHANT_CODE=9990088813REDSYS_TERMINAL=0014REDSYS_ENVIRONMENT=sandbox5REDSYS_NOTIFICATION_URL=https://your-api.com/hooks/payment/redsys_redsys6REDSYS_SUCCESS_URL=https://your-store.com/checkout/redsys-callback7REDSYS_ERROR_URL=https://your-store.com/checkout/redsys-callback?error=11Merchant Code: 9990088812Terminal: 0013Secret Key: sq7Hj.......4Environment: sandbox1import { defineConfig } from "@medusajs/framework/config"2
3export default defineConfig({4 modules: [5 {6 resolve: "@medusajs/medusa/payment",7 options: {8 providers: [9 {10 resolve: "@jsm406/medusa-plugin-redsys/providers/redsys",11 id: "redsys",12 options: {13 secretKey: process.env.REDSYS_SECRET_KEY,14 merchantCode: process.env.REDSYS_MERCHANT_CODE,15 terminal: process.env.REDSYS_TERMINAL || "001",16 environment:17 process.env.REDSYS_ENVIRONMENT || "sandbox",18 notificationUrl:19 process.env.REDSYS_NOTIFICATION_URL,20 successUrl: process.env.REDSYS_SUCCESS_URL,21 errorUrl: process.env.REDSYS_ERROR_URL,22 transactionType: "0", // "0" = immediate capture, "1" = pre-authorization23 },24 },25 // Bizum provider (optional - uses same credentials)26 {27 resolve: "@jsm406/medusa-plugin-redsys/providers/redsys-bizum",28 id: "redsys-bizum",29 options: {30 secretKey: process.env.REDSYS_SECRET_KEY,31 merchantCode: process.env.REDSYS_MERCHANT_CODE,32 terminal: process.env.REDSYS_TERMINAL || "001",33 environment:34 process.env.REDSYS_ENVIRONMENT || "sandbox",35 notificationUrl:36 process.env.REDSYS_BIZUM_NOTIFICATION_URL || process.env.REDSYS_NOTIFICATION_URL,37 successUrl: process.env.REDSYS_SUCCESS_URL,38 errorUrl: process.env.REDSYS_ERROR_URL,39 transactionType: "0",40 },41 },42 ],43 },44 },45 ],46})1// Inside paymentInfoMap, add:2pp_redsys_redsys: {3 title: "Credit / Debit Card",4 icon: <CreditCard />,5},6"pp_redsys-bizum_redsys-bizum": {7 title: "Bizum",8 icon: <Smartphone />,9},10
11// Add helper functions:12export const isRedsys = (providerId?: string) => {13 return providerId?.startsWith("pp_redsys_redsys") && !providerId?.includes("bizum")14}15
16export const isRedsysBizum = (providerId?: string) => {17 return providerId?.startsWith("pp_redsys-bizum")18}1// Redsys callback URL — bypass region redirect2if (request.nextUrl.pathname.startsWith("/checkout/redsys-callback")) {3 return NextResponse.next()4}1projectConfig: {2 http: {3 storeCors: "http://localhost:8000,https://your-store.com",4 },5}1{2 orderId: "1234ABCD5678", // Redsys merchant order (12 chars, ^\d{4}[A-Z0-9]{8}$)3 medusaSessionId: "payses_...", // Real Medusa payment session ID — never the Redsys order4 cartId: "cart_...", // Optional5 amount: "2550", // Smallest currency unit (cents)6 currency: "978", // Redsys numeric currency code7 status: "pending",8 transactionType: "0",9 merchantParams: "base64...", // Base64-encoded merchant parameters10 signature: "hmac...", // HMAC-SHA256 signature11 signatureVersion: "HMAC_SHA256_V1", // Version identifier returned by redsys-easy12 formUrl: "https://sis-t.redsys.es:25443/sis/realizarPago"13}1/hooks/payment/redsys_redsys (Card payments)2/hooks/payment/redsys-bizum_redsys-bizum (Bizum payments)1# Install dependencies2npm install3
4# Build5npm run build6
7# Run tests8npm test9
10# Watch mode (for local plugin development)11npm run dev1# From your plugin directory2npm run dev3
4# In your Medusa project directory:5npx medusa plugin:add ../path-to/@jsm406/medusa-plugin-redsys