django-oscar-redsys
Redsys (TPV Virtual) payment integration for django-oscar, using Redsys's redirection integration method (the customer's browser is sent to a Redsys-hosted payment page, per Redsys's own EMV 3DS/SCA support for that flow).
Supports Django 5.2 (LTS) and django-oscar 4.2 only — no support for older/unmaintained versions.
What this package does, and doesn't, do
It builds and signs the Ds_MerchantParameters/Ds_Signature payload
for a redirect (oscar_redsys.facade.RedsysFacade), provides the view
that receives and verifies Redsys's async notification
(oscar_redsys.views.NotificationView), and fires a
payment_confirmed/payment_declined Django signal
(oscar_redsys.signals) for the host project to hook into its own order
flow.
It does not decide what "payment confirmed" means for your Order
model, doesn't place orders, and doesn't ever trust the browser-redirect
(Ds_Merchant_UrlOK/Ds_Merchant_UrlKO) path to confirm a payment — only
the signed, server-to-server notification does that. See
oscar_redsys/views.py's module docstring for why.
Installation
uv add django-oscar-redsys
Add to INSTALLED_APPS:
INSTALLED_APPS = [
...,
"oscar_redsys",
]
Required settings (from Redsys's merchant portal — "Configuración del Comercio" → "Ver clave de firma"):
REDSYS_MERCHANT_CODE = "999008881"
REDSYS_TERMINAL = "1"
REDSYS_SECRET_KEY = "..." # keep this out of version control
REDSYS_SANDBOX = True # False in production
REDSYS_MERCHANT_URL = "https://example.com/redsys/notify/"
REDSYS_URL_OK = "https://example.com/redsys/return/?ok=1"
REDSYS_URL_KO = "https://example.com/redsys/return/?ok=0"
# REDSYS_CURRENCY defaults to "978" (EUR, ISO 4217)
Wire the notification/return endpoints:
urlpatterns = [
path("redsys/", include("oscar_redsys.urls")),
]
Run this package's migrations (oscar_redsys.RedsysNotification, the
notification audit/idempotency log, and oscar_redsys.RedsysOperation,
the refund/cancellation audit log) as part of the host project's own
migrate.
Building the redirect
Subclass oscar_redsys.views.PaymentRedirectView:
from decimal import Decimal
from oscar_redsys.views import PaymentRedirectView
class CheckoutPaymentView(PaymentRedirectView):
def get_order_number(self) -> str:
return self.request.basket.order_number # whatever generates yours
def get_amount(self) -> Decimal:
return self.request.basket.total_incl_tax
This renders an auto-submitting form pointed at Redsys's payment page.
get_order_number() must return Redsys's own format: at most 12
characters, the first 4 numeric, the rest (if any) plain ASCII
digits/letters — validated up front (ValueError if not), rather than
letting a malformed order fail opaquely at Redsys's end.
EMV3DS / SCA (optional, but improves checkout friction)
PSD2 requires Strong Customer Authentication (SCA) on most card payments; Redsys's EMV 3DS flow decides per transaction whether the issuer can authenticate the cardholder "frictionlessly" or must "challenge" them. Neither of the following is required for a payment to work, but supplying them lets more transactions go frictionless:
from oscar_redsys.emv3ds import Emv3dsData, MobilePhone, ScaExemption
class CheckoutPaymentView(PaymentRedirectView):
...
def get_emv3ds(self) -> Emv3dsData:
return Emv3dsData(
ship_addr_country="724", # numeric ISO 3166-1, e.g. Spain
cardholder_name=self.request.user.get_full_name(),
email=self.request.user.email,
mobile_phone=MobilePhone(country_code="34", subscriber="600123456"),
)
def get_sca_exemption(self) -> str | None:
if self.get_amount() <= Decimal("30.00"):
return ScaExemption.LOW_VALUE
return None
ScaExemption only defines the three values confirmed against Redsys's
own PSD2/SCA documentation (LWV, TRA, MIT) — pass any other code
Redsys documents as a plain string, it isn't restricted to these.
Consumer language (cosmetic)
get_consumer_language() sets what language Redsys's own hosted pages
(the payment form, the "Recibo Redsys" confirmation screen) render in —
it has no effect on the payment itself:
from oscar_redsys.language import ConsumerLanguage
class CheckoutPaymentView(PaymentRedirectView):
...
def get_consumer_language(self) -> str | None:
return ConsumerLanguage.SPANISH if self.request.LANGUAGE_CODE == "es" else ConsumerLanguage.ENGLISH
Handling the outcome
from django.dispatch import receiver
from oscar_redsys.signals import payment_confirmed, payment_declined
@receiver(payment_confirmed)
def on_payment_confirmed(sender, order_number, notification, **kwargs):
# place the order / trigger the Dolibarr sync chain / etc.
...
@receiver(payment_declined)
def on_payment_declined(sender, order_number, notification, **kwargs):
...
Both signals fire at most once per order number — a resent Redsys
notification (Redsys explicitly documents that it can resend) is a no-op
the second time, per RedsysNotification's uniqueness on order_number.
Refunds and cancellations — staff/admin only, never storefront
Unlike a payment, a refund or cancellation has no browser redirect at
all: it's a direct server-to-server call (oscar_redsys.rest, Redsys's
separate REST channel) referencing the original order, with no customer
involved. This package deliberately exposes it only as two Django
admin actions on RedsysNotification — "Refund selected payments" and
"Cancel selected payments" — never as a storefront view or URL a shopper
could reach.
Both actions are gated by the oscar_redsys.can_refund_or_cancel
permission on top of Django admin's own is_staff requirement — a
plain staff user can view payment records but can't trigger either
action without that permission explicitly granted (grant it via Django's
own admin, Permission model, or a group). Every attempt, successful or
not, is logged to RedsysOperation (who triggered it, when, the amount,
the outcome) — nothing here fires silently.
Refunds default to the full originally-authorized amount (recovered from
the stored notification's own Ds_Amount/Ds_Currency); there's no
partial-refund UI yet — see oscar_redsys.rest.build_refund_request if
you need a different amount from your own code.
Signature algorithm
Two versions, both verified byte-exact against Redsys's own published
worked examples in tests/test_signature.py — see
oscar_redsys/signature.py's module docstring for the full detail:
- V2 (
HMAC_SHA512_V2) for the redirection/browser flow, per "TPV-Virtual Manual de Integración - Redirección" (v4.1, 24/09/2025). - V1 (
HMAC_SHA512_V1) for the server-to-server REST channel (confirm/refund/cancel), per "TPV-Virtual Manual Integración-REST" (v4.0.1.1, 17/10/2025).
Both derive a per-operation key via AES-128-CBC (merchant secret key forced to 16 bytes, zero IV, diversified by the order number, then base64-encoded — that base64 string's ASCII bytes are the actual HMAC key, not the raw ciphertext, a detail easy to get wrong silently since a self-consistent sign/verify round trip "works" either way and only breaks against Redsys itself). They differ only in the final encoding: V2 uses URL-safe base64 with padding stripped, V1 uses standard base64 with padding kept.
Development
uv sync
uv run pytest
uv run mypy src
uv run black --check src tests
uv run isort --check src tests
License
BSD-3-Clause.
Metadata
Release files for django-oscar-redsys 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| django_oscar_redsys-0.1.0.tar.gz | 96.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| django_oscar_redsys-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 125.6 kB
Release files / django_oscar_redsys-0.1.0.tar.gz
| Download URL | django_oscar_redsys-0.1.0.tar.gz |
|---|---|
| Size | 96.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
03c1c9166cf19c6875e300296c1eca7887ee2255867b281a06fe6e305d7338a3
|
|
BLAKE2b-256 checksum How to use checksums |
3beb19aa0bd2454cbc014c6f36ed87778cfb75e5ea255d27c3795f204879c81b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.9 {"installer":{"name":"uv","version":"0.12.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / django_oscar_redsys-0.1.0-py3-none-any.whl
| Download URL | django_oscar_redsys-0.1.0-py3-none-any.whl |
|---|---|
| Size | 29.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a4cfa5cf63f090fb86b1b16ac44bb11b73b363481386b9b76c82ba4e7bc55b73
|
|
BLAKE2b-256 checksum How to use checksums |
02f874c328cb5574ebd04384a4645797fa8fde3f59d4fef094b3609ae616e6fb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.9 {"installer":{"name":"uv","version":"0.12.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|