creagia/redsys-php
PHP package for Redsys payment gateway integration. Generate signed payment requests, redirect customers to the Redsys TPV, and validate notifications with a simple API. Ideal for Spanish online stores needing secure card payments through Redsys, with helpers for signing and response handling.
creagia/redsys-php is a framework-agnostic PHP SDK for Redsys (the old Sermepa / TPV Virtual), the gateway behind most Spanish acquirers — Santander, BBVA, Sabadell, CaixaBank, Bankinter. If a Spanish client hands you a "FUC", a "terminal" and a base64 "clave secreta", this is the package you want. It is not a Laravel package: there is no service provider, no config file to publish, no facade. You get value objects and signature handling; the Laravel wiring is yours.
composer require creagia/redsys-php
Read in this order:
README.md — the redirect + notification happy path, which is 90% of real integrations.src/RedsysClient.php — the only object that holds credentials and environment.src/Enums/ — Currency, ResponseCode, and (as of 3.0.3) the Apple Pay / Google Pay pay-method case.src/RedsysRequest.php / src/RedsysNotification.php — outbound request build and inbound callback verification.First use case — a redirect checkout. Bind the client as a singleton so credentials come from config/services.php rather than being inlined:
// config/services.php
'redsys' => [
'merchant_code' => env('REDSYS_MERCHANT_CODE'),
'terminal' => env('REDSYS_TERMINAL', 1),
'secret_key' => base64_decode(env('REDSYS_SECRET_KEY')),
'environment' => env('REDSYS_ENVIRONMENT', 'test'),
],
$this->app->singleton(RedsysClient::class, fn () => new RedsysClient(
merchantCode: config('services.redsys.merchant_code'),
terminal: (int) config('services.redsys.terminal'),
secretKey: config('services.redsys.secret_key'),
environment: config('services.redsys.environment'),
));
Then: create a local orders row (status = pending, your own 12-char reference, amount in cents), build the request with the order reference and both URL callbacks, and render the auto-submitting form to sis-t.redsys.es:25443 (test) / sis.redsys.es (live) from the package's generated POST data. Test end-to-end with the classic sandbox card 4548 8120 4940 0004, any future expiry, CVV 123 before you go anywhere near production keys.
Treat the notification (server-to-server POST) as the only source of truth. The user's browser returning to your ok URL proves nothing — it is a GET the user can fake. Redirect them to a "processing" page, and let the notification handler flip the order to paid. This is the single most important architectural decision in a Redsys integration.
Route::post('/redsys/notify', RedsysNotificationController::class)
->withoutMiddleware([VerifyCsrfToken::class]); // or exclude in bootstrap/app.php
Verification order inside the handler (do it in this sequence, every time):
RedsysNotification and verify the signature with the client — reject on failure, log loudly.Ds_Order; 404 if unknown (someone is probing you).Ds_Amount against the stored amount in cents — a valid signature on the wrong amount still means fraud or a bug.Ds_Response is in the 0000–0099 authorised range.paid, return HTTP 200 and do nothing. Redsys retries.Keep the SDK out of your domain. Wrap it in a thin RedsysGateway service that takes your Order model and returns a DTO (redirectUrl, formData) or a PaymentResult. Controllers never touch RedsysClient, and you can swap to a different acquirer without rewriting checkout.
Refunds and cancellations reuse the same client. Build a request with the original order reference and the refund transaction type (3 for refund, 9 for cancellation), signed with the same secret. Store the original Ds_AuthorizationCode alongside the order so support can reconcile in the bank's panel.
Feature-test the callback, don't stub the SDK. Record one real notification payload from the sandbox (the full Ds_SignatureVersion / Ds_MerchantParameters / Ds_Signature trio), commit it as a fixture, and assert your controller marks the order paid. Add a second fixture with a tampered signature to assert rejection.
Wallets (new in 3.0.3). The Apple Pay / Google Pay case added in this release lets you carry a wallet-originated payment through the same request/notification path instead of hand-rolling Ds_Merchant_PayMethods. If you have a mobile app and were previously branching outside the SDK, this is the moment to collapse that custom code into the package's case. For everyone else it changes nothing.
1000 = €10.00. Sending 10.00 silently produces a €0.10 charge or a signature error depending on the acquirer.Ds_Merchant_Order is constrained: 12 characters max, first four numeric, and it must be unique forever. A common pattern is date('md') + an incrementing ID padded. Refunds require the exact original string, so persist it verbatim — never derive it at read time..env value.www. A mismatch surfaces as a generic gateway error page, not an exception in your logs.match on the package's enums will break on patch releases. 3.0.3 adds a wallet case; if you wrote match ($response->payMethod) { ... } with no default arm, you get an UnhandledMatchError at runtime for that traffic. Always include a default when matching enums you don't own — this is the one real upgrade risk in this release.0101 expired card, 0102 temporary bank refusal, 0190 denied by issuer) are far more legible than your own abstraction — surface them to support staff.Ds_Merchant_EMV3DS parameter set matters if you're doing in-app or MIT. Read the acquirer's manual for your specific merchant configuration before improvising; the package just passes the payload through.On the 3.0.3 upgrade itself: additive, no breaking changes, no config or migration steps. composer update creagia/redsys-php and ship — provided you check for exhaustive enum match statements (see above).
How can I help you explore Laravel packages today?