Overview
Take card and Bizum payments on OpenCart 4.1 through the Redsys TPV Virtual terminal your Spanish bank gave you. The customer pays on the secure Redsys payment page (3D Secure is done there); your shop marks the order paid only after the signed Redsys notification arrived and checked out. Full and partial refunds from the order page.
What it does
- Card and Bizum. Two checkout options: card (every method your terminal offers on the Redsys page) and Bizum (opens the Redsys page directly on Bizum, for euro orders). Each can be switched off and renamed.
- Signed both ways. The payment form is signed with HMAC_SHA256_V1 (default) or HMAC_SHA512_V2. The notification is verified with the terminal key of the mode the payment was made in, compared in constant time, and its amount, currency and merchant code must match.
- Paid means paid. The customer's return proves nothing; only the signed notification does. If the order changed while the customer paid, the money is booked on the payment and flagged, never marked paid: you accept or refund it with one click. A second payment of the same order is flagged as a duplicate to refund.
- Pending is never "cancelled". Bizum confirmations and slow notifications show a waiting page; the basket is emptied so nothing is paid twice, and the page moves on by itself.
- OpenCart 4.1 strict cookies handled. The customer comes back through a small bounce page (
extension/kv_redsys/return.php), so the success page, the emptied cart and a signed-in account keep working. - Refunds you can trust. Full or partial, through the Redsys REST interface, stored before they are sent. A refund without a clear answer is never repeated blindly: you check the Redsys portal and confirm it. Optional automatic refund when an order reaches a status you choose.
- Every attempt counts. Each attempt gets a new 12-character Redsys order number; the order tab lists all attempts of an order.
- Updates keep your data. OpenCart 4 updates are uninstall + install: payments, refunds, settings and the encrypted terminal keys survive.
Requirements
- OpenCart 4.1.0.x, PHP 8.1 to 8.3 (as OpenCart 4.1 requires) with cURL and OpenSSL, HTTPS on the live shop.
- A Redsys TPV Virtual terminal from your bank (merchant code, terminal number, terminal key); Bizum activated by the bank if you offer it. Bank fees apply.
Not in 1.0
Pre-authorization with later confirmation, saved cards (tokens), payment in instalments, InSite (card form on your own page). OpenCart 3 is not supported (no OpenCart 3 package is shipped).
Redsys is a trademark of Redsys Servicios de Procesamiento, S.L.; Bizum is a trademark of Bizum, S.L. This extension is independent software by Kavio Labs that integrates with the Redsys TPV Virtual; it is not affiliated with or endorsed by Redsys or Bizum.
Installation
- In the OpenCart admin open Extensions > Installer and upload
kv_redsys.ocmod.zip. Click the green install button next to it. - Open Extensions > Extensions, choose Payments, find Payments for Redsys and click Install, then Edit.
- Enter your terminal on the Connection tab (see Configuration), switch Status on and save.
- Make sure the OpenCart cron runs (the server line is shown on the Notification, log and cron tab).
No OCMOD refresh is needed: the extension uses events only.
Updating
Upload the new kv_redsys.ocmod.zip in the Installer and install it again (OpenCart 4 runs uninstall + install). Your payments, refunds, settings and the encrypted terminal keys are kept, so older orders can still be refunded.
Uninstalling
Uninstall keeps the payment and refund records and the settings. To delete everything, use Remove all data on the Notification, log and cron tab first (refused while a payment of the last 7 days is pending, a refund is unconfirmed or a payment does not match its order).
Configuration
Connection tab
| Field | What to enter |
|---|
| Mode | Test (sis-t.redsys.es, no real money) or Live (sis.redsys.es). Test mode is hidden from customers unless you tick the confirmation |
| Merchant code (FUC) | From the Redsys administration portal, up to 9 digits |
| Terminal | Terminal number, usually 1 |
| Terminal key | The merchant key of the terminal (clave de comercio). Stored encrypted, never shown again; leave empty to keep it |
| Signature version | HMAC_SHA256_V1 (default) or HMAC_SHA512_V2. The same key works for both. If Test connection answers SIS0042 with V2, use V1 |
Save and test connection sends a signed refund request for an order number that does not exist (0.01 EUR). It moves no money; "accepted by Redsys" means merchant code, terminal, key and signature are right.
For a first test without your own test terminal, Redsys publishes a shared test terminal and test cards in its developer area ("Tarjetas y entornos de prueba").
Payment tab
- Card and Bizum: switch each option on or off and rename it. Bizum is offered for euro orders and must be activated on your terminal.
- Currencies of your terminal: ISO codes your bank set up (most terminals:
EUR). The method appears only when the checkout currency is listed. Amounts are sent in the minor units of the currency. - Payment description: shown on the Redsys page and in the portal, followed by the order number.
- 3D Secure data: sends the customer's e-mail and name to help the bank approve without a challenge.
- Geo zone and sort order.
Statuses and refunds tab
- Paid: set when the signed notification confirms the payment (default Processing).
- Fully refunded: set when everything paid is refunded from the order page (default Refunded).
- Refund at status: when an order changes to this status, everything paid and not refunded yet is refunded automatically (off by default).
- E-mails: send the OpenCart order status e-mail when the payment is confirmed.
Notification, log and cron tab
The notification URL (<shop>/extension/kv_redsys/notify.php) is sent with every payment; nothing has to be entered in the Redsys portal, but your terminal must have the online notification switched on, and the live shop must answer over HTTPS on port 443. Debug log, cron command and Remove all data are on this tab too.
Usage
At checkout
The customer chooses Credit or debit card or Bizum, presses Continue to payment and pays on the Redsys page. Back in the shop:
- Paid: the order confirmation page; the order is in your Paid status.
- Declined or cancelled: the cart page explains it; the basket is kept and the customer can pay again (a new Redsys order number is used).
- Not confirmed yet (Bizum, slow notification): a waiting page. The basket is emptied so the order cannot be paid twice; the page moves on by itself when the notification arrives.
On the order page
Orders paid through Redsys have a Redsys tab listing every attempt: Redsys order number, answer code, authorization code, paid, refunded and refundable amounts.
- Refund: enter an amount (empty = the rest) and an optional note. Partial refunds can be repeated up to the paid amount.
- Unconfirmed refund: when Redsys gives no clear answer, check the operation in the Redsys portal and click It was refunded or It was not. Nothing is repeated automatically.
- Accept as paid: for a payment whose amount differs from the order (the order changed while the customer paid). Or refund it instead.
- Duplicate: a second payment of an already paid order is marked; refund it from the tab.
Settings page warnings
The settings page warns about mismatched payments, unconfirmed refunds and attempts that got no notification within an hour (a sign that the notification does not reach your shop).
Developer notes
Technical reference for developers who customize Payments for Redsys on OpenCart 4.1. The payment logic is a port of our Redsys add-on for CS-Cart (1.0.1, after its release audit), written for OpenCart's checkout, order model and events.
Extension identity
| Item | Value |
|---|
| Extension code | kv_redsys (payment extension, extension/kv_redsys/) |
| Settings | payment_kv_redsys_* |
| Shared core namespace | KavioLabs\KvRedsys (system/library/ of the extension) |
| Checkout options | kv_redsys.card and kv_redsys.bizum |
| Events | admin/view/sale/order_info/before (Redsys tab), admin/controller/common/header/before (licence check), catalog/model/checkout/order.addHistory/after (refund at a status), `catalog/controller/checkout/checkout | cart | confirm | confirm.confirm | cart.add/before and catalog/controller/common/cart/before (session guard src/CheckoutGuard.php`, see Payment flow 5) |
| Cron | OpenCart cron kv_redsys (hourly) or index.php?route=extension/kv_redsys/payment/kv_redsys.cron&key=<key> |
| OCMOD | none, core files are never changed |
Structure
| Class | Role |
|---|
Signature | Message signing, pure functions: HMAC_SHA256_V1 and HMAC_SHA512_V2, per-order key, Base64 / Base64URL, verification of notifications and REST answers (hash_equals over the normalised Base64 text) |
Codes | Ds_Response and SIS error texts, ISO 4217 numeric currency codes (three digits), Ds_Merchant_ConsumerLanguage values |
Money | ISO 4217 exponents from the complete List One (checked by tooling/release-templates/cscart/iso4217/gen.py --check), exact decimal to minor units |
Api | The terminal of one mode: signed redirection fields, REST operation (refund type 3, connection probe) |
Payments | Attempt start, notification handling, customer state, refunds, unconfirmed refunds, refund at a status, cron |
Orders | Order amount in its currency with the stored rate, order history through OpenCart's order model (also inside the admin order editor), saved cart removal |
Webhook | Reads the notification form fields from the raw POST, keeps only the fields that are used (no card data) |
Config, Secret, Keep | Settings, terminal keys sealed with AES-256-GCM, settings snapshot kept across updates (OpenCart 4 updates are uninstall + install) |
SameSite + return.php | Customer return that keeps the SameSite=Strict session of OpenCart 4.1 |
notify.php | Parameter-free notification endpoint with one fixed route |
Payment flow
- Checkout: the customer picks "card" or "Bizum" and presses Continue to payment.
start (AJAX) checks that the session order is unpaid, its currency is one of the terminal currencies (Bizum: euro only), creates a unique 12-character Redsys order number and stores the attempt row (pending) before it returns the three signed fields. The browser posts them to .../sis/realizarPago. - Redsys posts the signed notification to
extension/kv_redsys/notify.php (DS_MERCHANT_MERCHANTURL). The attempt is found by Ds_Order; the signature is verified with the terminal key of the mode the attempt was made in; merchant code, amount and currency (always required) must match. A paid answer gives the order the "Paid" status through model_checkout_order->addHistory() (stock, confirmation e-mail, other extensions' events), unless the order total changed since the attempt started (money booked, order not paid, flagged) or another attempt already paid the order (flagged as a duplicate to refund). A notification that cannot be applied at once (lock busy, order model error) is kept on the row and applied by the cron; Redsys still gets 200. - The customer returns on DS_MERCHANT_URLOK / URLKO through
extension/kv_redsys/return.php (which boots nothing and continues same-site to the back route). The return carries nothing that is trusted. The page waits up to 5 seconds for the notification, then: paid with the order in its paid status = success page; declined or cancelled = cart page with the message (basket and unpaid order kept, a new attempt gets a new order number); anything else (pending, paid while the order status is still being set, paid for a changed order) = basket emptied (also the saved cart rows of a signed-in customer) and a waiting page that follows the payment. A pending payment is never shown as cancelled. - Refunds:
Payments::refund() locks the attempt's refunds, stores a refund row (creating) before the REST call and sends operation type 3 with the order number of the payment. A SIS refusal or a Ds_Response other than 0900 = failed; a timeout, an unreadable or unsigned answer or 912 / 9912 = unconfirmed: the amount stays reserved, nothing repeats it, and an administrator confirms on the order tab whether it was executed (atomic claim, booked once). - Session guard: Redsys shows its receipt by default and the customer may close it instead of coming back. The session then still holds
order_id and the cart. OpenCart 4.1.0.4 does not edit an order that already has a status (checkout/confirm.php:281), but the next checkout visit still works on that order: the payment button, the comment box, and the basket that still holds the paid goods (an "add to basket" drops the session order and the paid goods would go into the next order). Before checkout/checkout, checkout/cart, checkout/confirm(.confirm), checkout/cart.add and the header mini basket (common/cart) the extension checks whether the session order already took money through Redsys or has a status; if so, the basket (and a signed-in customer's saved cart rows) is emptied and the customer goes to the confirmation page (checkout and basket pages) or the session order is dropped (the other triggers). A new start for a paid order goes to the confirmation page as well. - Locks: starting an attempt, and booking a paid notification ("already paid?", update, order status) run under a per-order database lock in addition to the per-attempt lock, so two attempts notified at the same moment cannot both become the payment.
Official sources and every field
Documents of the Redsys developer area (pagosonline.redsys.es, "Area de descargas y documentacion"), read for the CS-Cart add-on on 2026-11-07 and re-read for this extension on 2026-11-08:
| Short name | Document |
|---|
| REDIR | Guia de Integracion por Redireccion, ref. RS.TE.CEL.MAN.0039, v4.2 |
| REST | Guia de integracion por REST, ref. RS.TE.CEL.MAN.0037, v4.0.1.1 (Annex 2 = HMAC SHA256) |
| BIZUM | Guia de Integracion BIZUM comercios SIS, ref. RS.TE.CEL.MAN.0033, v2.6 |
| IO | "Parametros de entrada y salida" page of the developer area |
| TEST | "Tarjetas y entornos de prueba" page of the developer area |
| REFUND | "Devolver o anular un pago" page of the developer area |
| PHPLIB | Redsys PHP library 2.0.1: read only to confirm algorithm details (it still generates HMAC_SHA256_V1, HMAC_SHA512_V1 and HMAC_SHA512_V2). Its licence forbids redistribution; none of its code is shipped |
Endpoints
| Use | URL | Source |
|---|
| Redirection, test / live | https://sis-t.redsys.es:25443/sis/realizarPago / https://sis.redsys.es/sis/realizarPago | REDIR, TEST |
| REST, test / live | https://sis-t.redsys.es:25443/sis/rest/trataPeticionREST / https://sis.redsys.es/sis/rest/trataPeticionREST | REST, REFUND |
Redirection form (HTTP POST from the customer's browser)
Ds_SignatureVersion (HMAC_SHA256_V1 default, or HMAC_SHA512_V2), Ds_MerchantParameters (JSON of the DS_MERCHANT_* parameters; standard Base64 for V1, Base64URL without padding for V2), Ds_Signature (REDIR 5, 6.1; REST Annex 2).
| Parameter | What is sent | Source |
|---|
DS_MERCHANT_AMOUNT | ISO minor units as an integer string (43.45 EUR = 4345, JPY 0 decimals, KWD 3) | IO |
DS_MERCHANT_ORDER | 12 characters: 4 digits (order id mod 10000) + 8 random base-36, new for every attempt, unique key in the database | IO; BIZUM ^[a-zA-Z0-9]{4,12}$; REDIR SIS0051 |
DS_MERCHANT_MERCHANTCODE, DS_MERCHANT_TERMINAL | Merchant code (FUC, up to 9 digits), terminal | IO, TEST |
DS_MERCHANT_CURRENCY | ISO 4217 numeric code, three digits (978) | IO |
DS_MERCHANT_TRANSACTIONTYPE | 0 (authorization) | IO, BIZUM 2.2.1 |
DS_MERCHANT_MERCHANTURL | <shop>/extension/kv_redsys/notify.php, max 250 characters | IO, REDIR 7 |
DS_MERCHANT_URLOK / URLKO | <shop>/extension/kv_redsys/return.php?txn=..&k=..[&ko=1], max 250 characters | IO |
DS_MERCHANT_PAYMETHODS | z (lower case) for the Bizum option only | BIZUM 2.2.1 |
DS_MERCHANT_PRODUCTDESCRIPTION | Description + #order id, cut to 125 characters (length not verified in a public document) | PHPLIB |
DS_MERCHANT_CONSUMERLANGUAGE | 001 es, 002 en, 003 ca, 010 va, 012 gl, 013 eu; other languages omit it | REDIR, PHPLIB |
DS_MERCHANT_EMV3DS | {"email", "cardholderName"} when the setting is on | REDIR 6.1.4 example |
Signature
| Step | HMAC_SHA256_V1 (REST Annex 2) | HMAC_SHA512_V2 (REDIR 6.1.2, 6.1.3) |
|---|
| Terminal key | Base64-decoded (24 bytes) | first 16 characters, right-padded with 0 |
| Per-order key | 3DES-CBC, zero IV, order number zero-padded to 8 bytes | AES-128-CBC, zero IV; its Base64 text is the HMAC key |
| Signature | HMAC-SHA256 over the Ds_MerchantParameters string, Base64 | HMAC-SHA512, Base64URL without padding |
Verification decodes Ds_MerchantParameters only to read Ds_Order, signs the string exactly as received and compares with hash_equals after normalising the Base64 alphabet and padding. The REST guide states that "the same merchant key serves both HMAC SHA512 and HMAC SHA256".
Test vectors asserted in tests/unit.php: V2 derived key RWt3/IPTzYRMXsQtkiGRKg== and signature Vjo02eSW... (REDIR 6.1.2/6.1.3), V1 derived key a5334014a4f010c8779cef789886c123 and signature GmHwTovthyrztLs7D77GflclBzsANderHe3zFF6JiZQ= (REST Annex 2), test terminal 999008881 / 1 (TEST).
Notification
Ds_Response 0000 to 0099 = paid; 9915 = cancelled by the customer; 8210, 8220, 9998, 9999 or empty = still pending (never a failure); anything else = declined (REDIR "Resultado de la Operacion"). Only Ds_TransactionType 0 changes a payment. Ds_Amount and Ds_Currency must equal what was sent; Ds_MerchantCode must be the attempt's. The guide states that the result must be taken only from this POST, never from the OK/KO URLs.
REST refund (operation type 3)
JSON body {"Ds_SignatureVersion", "Ds_MerchantParameters", "Ds_Signature"} with DS_MERCHANT_ORDER (the payment's order number), merchant code, terminal, DS_MERCHANT_TRANSACTIONTYPE=3, currency and amount; total or partial; Ds_Response 0900 = refunded; failures {"errorCode":"SIS...."} (REST, REFUND). Bizum payments are refunded the same way (BIZUM 2.3). Client timeout 50 s (REST "Timeout": Redsys answers within 30 s). The answer's signature is verified.
Connection probe
A signed type 3 refund of the non-existing order 0000KVPROBE0 for 0.01 EUR: SIS0054 ("no operation to refund") means merchant code, terminal and signature were accepted; SIS0042 / SIS0026 / SIS0027 mean they were not. The meaning of SIS0054 for a probe is inferred from the error table.
Flagged: not verifiable from public documents
Ds_Merchant_PayMethods values other than z (the table is not public); the card option therefore omits the parameter (every method of the terminal is shown).- Field lengths of
DS_MERCHANT_PRODUCTDESCRIPTION and DS_MERCHANT_EMV3DS; language codes other than the six listed. - REDIR 4.2 calls HMAC_SHA512_V2 the current value; REST 4.0.1.1 documents SHA512 and, in Annex 2, HMAC_SHA256_V1. The default is HMAC_SHA256_V1 (documented with a test vector for REST and still generated by the official library for redirection); switch to V2 if your bank asks for it.
- Which IP addresses Redsys notifies from: do not IP-filter the notification URL; the signature is the protection.
- Bizum: every example of the Bizum guide uses currency 978; the Bizum option is offered for euro orders only.
Database tables
| Table | Purpose |
|---|
kv_redsys_txn | One row per attempt: Redsys order number (unique), mode, merchant code, terminal, signature version, amounts in minor units, order total at the start, status, answer, authorization code, duplicate / mismatch flags, kept notification |
kv_redsys_refund | Refund rows: creating, done, failed, unconfirmed, resolving |
kv_redsys_log | Log (60 days) |
Uninstall keeps all three and the settings (Keep snapshot); "Remove all data" on the settings page deletes them.
Test the HTTP layer without Redsys
The hidden setting payment_kv_redsys_base_url points both modes to a local mock; it is honoured for loopback hosts only. node tests/mock-server/server.js 11841 starts the stand-in (both signature versions, order number rules, SIS0051, signed notifications, customer Pay / Decline / Cancel buttons, REST refunds with timeout / unsigned / 912 modes); php tests/unit.php runs the unit tests and node tests/e2e.js the browser test. This proves the extension against the documentation, not against the Redsys test environment.
Trademarks
Redsys is a trademark of Redsys Servicios de Procesamiento, S.L.; Bizum is a trademark of Bizum, S.L. This extension is not affiliated with or endorsed by either. No Redsys or Bizum logo is bundled.
FAQ
Do I need a Redsys contract?
Yes: a TPV Virtual terminal from your bank (merchant code, terminal, key). The extension uses your terminal; Redsys and your bank charge their own fees.
Which signature version should I use?
HMAC_SHA256_V1 is the default and is documented for both the payment form and refunds. HMAC_SHA512_V2 is the newer version of the redirection guide; the same key signs both.
Why is the method not shown at checkout?
Check: Status on; merchant code and key of the selected mode saved; the checkout currency listed under "Currencies of your terminal"; in Test mode, the confirmation ticked; the geo zone.
A customer paid but the order is still missing. The notification did not reach the shop. Check that the terminal has the online notification on and that your shop answers on HTTPS port 443. The settings page counts attempts without a notification. Late notifications are still accepted.
Can a customer pay twice?
Each attempt gets its own order number. A second payment of an already paid order is flagged on the order tab so you can refund it.
Does it support Apple Pay or Google Pay?
If your bank enabled them on your Redsys terminal, they appear on the Redsys page of the card option.
Can I change the terminal key?
Yes, but do it when no payment is open: notifications of payments started with the old key are refused (the settings page counts payments without a notification).
OpenCart 3?
No, OpenCart 4.1 only.