• Сообщество
  • Блог
Документация
Плагины и интеграцииВсе расширения для Medusa от сообществаЭкспертыПодберите специалиста для разработки и развития вашего проекта на Medusa
КейсыПосмотрите примеры Medusa в продакшене и успешные внедрения
Меч Moscow
Комплексная e-commerce платформа на Medusa для московского fashion-бренда

Меч Moscow · Fashion

Gorgo снижает затраты на адаптацию Medusa к локальным рынкам.

Мы разрабатываем плагины интеграции, осуществляем поддержку и развиваем сообщество разработчиков на Medusa в Telegram.

  • Ресурсы Medusa
  • Плагины и интеграции
  • Эксперты
  • Кейсы
  • Medusa Чат в Telegram
  • Medusa Новости в Telegram
  • Документация Gorgo
  • Связаться с нами
  • TelegramGitHub
Плагины
R

Redsys

Redsys / Sermepa TPV Virtual payment provider plugin for MedusaJS v2

npm install @jsm406/medusa-plugin-redsys
Категория
Платежи
Создано
Jsm406
Версия
1.1.0
Последнее обновление
2 недели назад
Ежемесячные загрузки
Загрузка данных
Звезды на Github
0
npmNPMGitHubGithub

@jsm406/medusa-plugin-redsys

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.

Features

  • Redsys hosted payment page / TPV Virtual redirect flow
  • Bizum mobile payment support via Redsys TPV
  • Sandbox and production environments
  • One-step payment (immediate capture) and two-step payment (pre-authorization + capture)
  • Full and partial refunds via Redsys API
  • Payment cancellation
  • Webhook handling with HMAC-SHA256 signature verification
  • Spanish error messages for Redsys response codes
  • Zero PCI scope — card data is handled by Redsys' secure page

Prerequisites

  • MedusaJS v2.13.0 or later
  • Node.js v20 or later
  • A Redsys merchant account (or sandbox test credentials)
  • v5.3.0+ (installed automatically as a dependency)

Installation

1npm install @jsm406/medusa-plugin-redsys
2# or
3yarn add @jsm406/medusa-plugin-redsys
4# or
5pnpm add @jsm406/medusa-plugin-redsys

Configuration

Environment Variables

Add the following to your file:

1REDSYS_SECRET_KEY=sq7Hj....
2REDSYS_MERCHANT_CODE=999008881
3REDSYS_TERMINAL=001
4REDSYS_ENVIRONMENT=sandbox
5REDSYS_NOTIFICATION_URL=https://your-api.com/hooks/payment/redsys_redsys
6REDSYS_SUCCESS_URL=https://your-store.com/checkout/redsys-callback
7REDSYS_ERROR_URL=https://your-store.com/checkout/redsys-callback?error=1

For sandbox testing, use the following test credentials from Redsys:

Medusa Configuration

In your :

1import { 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-authorization
23 },
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})

Enable in Region

Enable the Redsys provider(s) in your Medusa admin panel under Settings > Regions:

  • Credit/Debit Card: Select Redsys as a payment provider
    • Provider ID:
  • Bizum: Select Redsys Bizum as a payment provider
    • Provider ID:

You can enable one or both providers depending on which payment methods you want to offer.

Options

OptionTypeRequiredDefaultDescription
stringYes—Redsys HMAC-SHA256 secret key
stringYes—Redsys merchant code (FUC)
stringNoTerminal number
stringNo or
stringNo—Webhook URL for Redsys to POST transaction results
stringNo—URL to redirect after successful payment (URLOK)
stringNo—URL to redirect after failed payment (URLKO)
stringNo = immediate capture, = pre-authorization

Payment Flow

  1. Customer selects Redsys as payment method
  2. creates a signed redirect form with Redsys merchant parameters
  3. Customer clicks "Place Order" → storefront calls to create the order, stores a cookie mapping the Redsys internal order ID to the Medusa order ID, then auto-submits the redirect form to Redsys TPV
  4. Customer completes payment on the Redsys hosted payment page
  5. Redsys sends a webhook notification to
  6. validates the HMAC-SHA256 signature and updates the payment status
  7. Redsys redirects the customer's browser to or with the Redsys order ID as a query parameter
  8. Storefront callback page reads the sessionStorage to resolve the Medusa order ID and redirects to the order confirmation page

Important: authorizePayment Behavior

This plugin's returns for sessions with status and . This is intentional for the redirect flow: the real authorization happens on Redsys TPV and is confirmed via webhook. Without this, would fail with a 400 error because Medusa requires the payment session to be authorized before completing the cart.

ID Mapping (Redsys → Medusa)

The plugin generates a 12-character alphanumeric (e.g. ) used as Redsys' merchant order reference. When the order is completed via , 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 reads this value to redirect to the correct order confirmation page.

Storefront Integration

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.

1. — Register the payment methods

Add Redsys and Bizum to the payment info map and add helper functions:

1// Inside paymentInfoMap, add:
2pp_redsys_redsys: {
3 title: "Credit / Debit Card",
4 icon: <CreditCard />,
5},
6pp_redsys_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_redsys_bizum")
18}

2. — Add order completion without redirect

Add a function. The standard does a (server-side), but Redsys needs to redirect the browser to the TPV instead. This function completes the cart, creates the order, but returns the result so the client can handle the TPV redirect:

1export async function completeCartWithoutRedirect(cartId?: string) {
2 const id = cartId || (await getCartId())
3
4 if (!id) {
5 throw new Error("No existing cart found when completing cart")
6 }
7
8 const headers = {
9 ...(await getAuthHeaders()),
10 }
11
12 const cartRes = await sdk.store.cart
13 .complete(id, {}, headers)
14 .then(async (cartRes) => {
15 const cartCacheTag = await getCacheTag("carts")
16 revalidateTag(cartCacheTag)
17 return cartRes
18 })
19 .catch(medusaError)
20
21 if (cartRes?.type === "order") {
22 const orderCacheTag = await getCacheTag("orders")
23 revalidateTag(orderCacheTag)
24 removeCartId()
25 }
26
27 return cartRes
28}

3. — Payment buttons

Add payment button components for both Redsys (card) and Bizum. Both use the same redirect flow but with different provider IDs.

1// Add imports:
2import { isManual, isRedsys, isRedsysBizum, isStripeLike } from "@lib/constants"
3import { completeCartWithoutRedirect, placeOrder } from "@lib/data/cart"
4
5// Add cases in PaymentButton's switch:
6case isRedsysBizum(paymentSession?.provider_id):
7 return (
8 <RedsysBizumPaymentButton
9 notReady={notReady}
10 cart={cart}
11 data-testid={dataTestId}
12 />
13 )
14
15case isRedsys(paymentSession?.provider_id):
16 return (
17 <RedsysPaymentButton
18 notReady={notReady}
19 cart={cart}
20 data-testid={dataTestId}
21 />
22 )
23
24// Redsys Card Payment Button:
25const RedsysPaymentButton = ({
26 cart,
27 notReady,
28 "data-testid": dataTestId,
29}: {
30 cart: HttpTypes.StoreCart
31 notReady: boolean
32 "data-testid"?: string
33}) => {
34 const [submitting, setSubmitting] = useState(false)
35 const [errorMessage, setErrorMessage] = useState<string | null>(null)
36
37 const handlePayment = async () => {
38 setSubmitting(true)
39
40 const paymentSession = cart.payment_collection?.payment_sessions?.find(
41 (s) => s.status === "pending" && isRedsys(s.provider_id)
42 )
43
44 const redsysData = paymentSession?.data as Record<string, string> | undefined
45
46 if (!redsysData?.formUrl || !redsysData?.merchantParams || !redsysData?.signature) {
47 setErrorMessage("No se pudieron obtener los datos de pago de Redsys")
48 setSubmitting(false)
49 return
50 }
51
52 const cartRes = await completeCartWithoutRedirect()
53 .catch((err) => {
54 setErrorMessage(err.message)
55 setSubmitting(false)
56 return null
57 })
58
59 if (!cartRes || cartRes.type !== "order") {
60 setErrorMessage(cartRes ? "Error al crear el pedido" : "")
61 setSubmitting(false)
62 return
63 }
64
65 const medusaOrderId = cartRes.order.id
66 const redsysOrderId = redsysData.orderId || ""
67 const countryCode = cart.shipping_address?.country_code?.toLowerCase() || "dk"
68 sessionStorage.setItem(
69 `redsys_map_${redsysOrderId}`,
70 JSON.stringify({ medusaOrderId, countryCode })
71 )
72
73 const form = document.createElement("form")
74 form.method = "POST"
75 form.action = redsysData.formUrl
76
77 const fields: Record<string, string> = {
78 Ds_SignatureVersion: redsysData.signatureVersion,
79 Ds_MerchantParameters: redsysData.merchantParams,
80 Ds_Signature: redsysData.signature,
81 }
82
83 Object.entries(fields).forEach(([name, value]) => {
84 const input = document.createElement("input")
85 input.type = "hidden"
86 input.name = name
87 input.value = value
88 form.appendChild(input)
89 })
90
91 document.body.appendChild(form)
92 form.submit()
93 }
94
95 return (
96 <>
97 <Button
98 disabled={notReady || submitting}
99 isLoading={submitting}
100 onClick={handlePayment}
101 size="large"
102 data-testid={dataTestId}
103 >
104 Place order
105 </Button>
106 <ErrorMessage
107 error={errorMessage}
108 data-testid="redsys-payment-error-message"
109 />
110 </>
111 )
112}
113
114// Bizum Payment Button (identical flow, different provider check):
115const RedsysBizumPaymentButton = ({
116 cart,
117 notReady,
118 "data-testid": dataTestId,
119}: {
120 cart: HttpTypes.StoreCart
121 notReady: boolean
122 "data-testid"?: string
123}) => {
124 const [submitting, setSubmitting] = useState(false)
125 const [errorMessage, setErrorMessage] = useState<string | null>(null)
126
127 const handlePayment = async () => {
128 setSubmitting(true)
129
130 const paymentSession = cart.payment_collection?.payment_sessions?.find(
131 (s) => s.status === "pending" && isRedsysBizum(s.provider_id)
132 )
133
134 const redsysData = paymentSession?.data as Record<string, string> | undefined
135
136 if (!redsysData?.formUrl || !redsysData?.merchantParams || !redsysData?.signature) {
137 setErrorMessage("No se pudieron obtener los datos de pago de Bizum")
138 setSubmitting(false)
139 return
140 }
141
142 const cartRes = await completeCartWithoutRedirect()
143 .catch((err) => {
144 setErrorMessage(err.message)
145 setSubmitting(false)
146 return null
147 })
148
149 if (!cartRes || cartRes.type !== "order") {
150 setErrorMessage(cartRes ? "Error al crear el pedido" : "")
151 setSubmitting(false)
152 return
153 }
154
155 const medusaOrderId = cartRes.order.id
156 const redsysOrderId = redsysData.orderId || ""
157 const countryCode = cart.shipping_address?.country_code?.toLowerCase() || "dk"
158 sessionStorage.setItem(
159 `redsys_map_${redsysOrderId}`,
160 JSON.stringify({ medusaOrderId, countryCode })
161 )
162
163 const form = document.createElement("form")
164 form.method = "POST"
165 form.action = redsysData.formUrl
166
167 const fields: Record<string, string> = {
168 Ds_SignatureVersion: redsysData.signatureVersion,
169 Ds_MerchantParameters: redsysData.merchantParams,
170 Ds_Signature: redsysData.signature,
171 }
172
173 Object.entries(fields).forEach(([name, value]) => {
174 const input = document.createElement("input")
175 input.type = "hidden"
176 input.name = name
177 input.value = value
178 form.appendChild(input)
179 })
180
181 document.body.appendChild(form)
182 form.submit()
183 }
184
185 return (
186 <>
187 <Button
188 disabled={notReady || submitting}
189 isLoading={submitting}
190 onClick={handlePayment}
191 size="large"
192 data-testid={dataTestId}
193 >
194 Pagar con Bizum
195 </Button>
196 <ErrorMessage
197 error={errorMessage}
198 data-testid="redsys-bizum-payment-error-message"
199 />
200 </>
201 )
202}

4. — Callback page (new file)

Create a client component page that Redsys redirects to after payment. It reads the query param (Redsys internal ID), looks up the real Medusa order ID from , and redirects to the order confirmation page:

1"use client"
2
3import { useRouter, useSearchParams } from "next/navigation"
4import { useEffect, useState } from "react"
5
6export default function RedsysCallbackPage() {
7 const searchParams = useSearchParams()
8 const router = useRouter()
9 const [status, setStatus] = useState<"loading" | "error" | "success">("loading")
10
11 const isError = searchParams?.get("error") === "1"
12 const redsysOrderId = searchParams?.get("orderId")
13
14 useEffect(() => {
15 if (isError) {
16 setStatus("error")
17 return
18 }
19
20 if (!redsysOrderId) {
21 setStatus("success")
22 return
23 }
24
25 const stored = sessionStorage.getItem(`redsys_map_${redsysOrderId}`)
26
27 if (stored) {
28 let orderData: { medusaOrderId: string; countryCode: string }
29 try {
30 orderData = JSON.parse(stored)
31 } catch {
32 orderData = { medusaOrderId: stored, countryCode: "dk" }
33 }
34 sessionStorage.removeItem(`redsys_map_${redsysOrderId}`)
35 router.replace(
36 `/${orderData.countryCode}/order/${orderData.medusaOrderId}/confirmed`
37 )
38 return
39 }
40
41 setStatus("success")
42 }, [isError, redsysOrderId, router])
43
44 if (status === "loading") {
45 return (
46 <div className="flex flex-col items-center justify-center min-h-[50vh] gap-4 p-8">
47 <p className="text-gray-600">Procesando pago...</p>
48 </div>
49 )
50 }
51
52 if (status === "error") {
53 return (
54 <div className="flex flex-col items-center justify-center min-h-[50vh] gap-4 p-8">
55 <h1 className="text-2xl font-bold text-red-600">Pago no completado</h1>
56 <p className="text-gray-600">
57 La operación no se ha completado correctamente.
58 </p>
59 <a href="/" className="mt-4 px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">
60 Volver a la tienda
61 </a>
62 </div>
63 )
64 }
65
66 return (
67 <div className="flex flex-col items-center justify-center min-h-[50vh] gap-4 p-8">
68 <h1 className="text-2xl font-bold text-green-600">Pago procesado</h1>
69 <p className="text-gray-600">Tu pago ha sido procesado correctamente.</p>
70 <a href="/" className="mt-4 px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">
71 Volver a la tienda
72 </a>
73 </div>
74 )
75}

5. — Bypass region redirect

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:

1// Redsys callback URL — bypass region redirect
2if (request.nextUrl.pathname.startsWith("/checkout/redsys-callback")) {
3 return NextResponse.next()
4}

6. — CORS

Ensure your storefront domain is allowed in CORS:

1projectConfig: {
2 http: {
3 storeCors: "http://localhost:8000,https://your-store.com",
4 },
5}

Session Data Reference

The payment session field returned by :

1{
2 orderId: "1234ABCD5678",
3 amount: "2550",
4 currency: "978",
5 status: "pending",
6 transactionType: "0",
7 merchantParams: "base64...", // Base64-encoded merchant parameters
8 signature: "hmac...", // HMAC-SHA256 signature
9 signatureVersion: "HMAC_SHA256_V1", // Normal - version identifier from redsys-es library
10 formUrl: "https://sis-t.redsys.es:25443/sis/realizarPago"
11}

Note: The identifier in the URL callback 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. The value is informational in the callback URL.

These fields are used in step 3 to build the auto-submitting redirect form.

Webhook

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.

Test Cards (Sandbox)

Card Payments

Card NumberBrandBehavior
4548810000000003VISA3DS v2 approved
5576441563045037Mastercard3DS v2 approved
4548814479727229VISA3DS frictionless
4548817212493017VISA3DS challenge
Any + CVV 999AnyPayment declined

Bizum (Sandbox)

Important: In sandbox, Bizum transactions cannot exceed 10€. Use a discount coupon or low-price test product.

FieldValue
Phone number
PIN
SMS code

Test scenarios by amount:

AmountResult
< 5€Payment approved
5€ - 10€Payment approved
10€ - 15€Payment declined (exceeds sandbox limit)
> 15€Payment declined (no Bizum user)

Transaction Types

CodeTypeDescription
PaymentAuthorization + immediate capture (default)
Pre-authorizationReserve funds only
ConfirmationCapture pre-authorized funds
RefundFull or partial refund
CancellationCancel/void a transaction

Security

  • Never log PAN, CVV, or the secret key. The provider strips sensitive fields from log output.
  • Always validate signatures server-side. uses 's for HMAC-SHA256 verification.
  • Use HTTPS for all communication with Redsys.
  • Do not trust client-side payment data. The webhook with signature verification is the source of truth.
  • The redirect flow keeps you out of PCI scope — card data is handled by Redsys' secure page.

Currency Support

The plugin includes built-in numeric currency codes for all major currencies. If your currency is not listed, it defaults to EUR (). See for the full list.

Development

1# Install dependencies
2npm install
3
4# Build
5npm run build
6
7# Run tests
8npm test
9
10# Watch mode (for local plugin development)
11npm run dev

Local Testing with a Medusa Project

1# From your plugin directory
2npm run dev
3
4# In your Medusa project directory:
5npx medusa plugin:add ../path-to/@jsm406/medusa-plugin-redsys

License

MIT — see LICENSE file for details.

Version History

v1.1.0 (2026-06-16)

  • Added: Bizum payment method support via Redsys TPV
  • New provider with parameter
  • Full lifecycle support: initiate, authorize, capture, cancel, refund, webhook
  • Same configuration as card provider (can share credentials)
  • Separate webhook endpoint at

v1.0.12 (2026-05-13)

  • Fixed: Response code validation for payment authorization (codes 0-99), refunds/confirmations (code 900), cancellations (code 400)
  • Note: The in the callback URL is normal - it's the identifier returned by the redsys-es library. The actual HMAC computation follows Redsys v4.1 specification.

v1.0.0 (2025-05-05)

  • Initial release

Support

For issues and questions, please open an issue on GitHub.

Еще в этой категории

Посмотреть все
Платежи
Braintree logo

Braintree

От Lambda Curry

Поддержка платежей и 3D Secure через Braintree

Загрузка данных
GitHubnpm
Платежи
Pay. logo

Pay.

От Webbers

Принимайте кредитные карты, цифровые платежи и купи сейчас — плати потом

Загрузка данных
GitHubnpm
Платежи
Mollie logo

Mollie

От Variable Vic

Легко принимайте мультивалютные платежи через Mollie

Загрузка данных
GitHubnpm