mendicm/sermepa-omnipay
Omnipay gateway driver for Sermepa (Redsys), the Spanish payment gateway. Adds Sermepa support to PHP and Laravel apps through Omnipay, so you can process card payments, handle redirects, and verify responses using a consistent, framework-agnostic API.
Sermepa is the legacy name for Redsys, the Spanish TPV (bank card processor) used by BBVA, Santander, Sabadell, and most Spanish acquirers. This package is an Omnipay driver, so it plugs into the generic Omnipay API rather than exposing its own client. That means most of your day-to-day knowledge transfers from any other Omnipay gateway.
composer require mendicm/sermepa-omnipay. It will pull omnipay/common with it. Check which major version it targets (composer show omnipay/common): Omnipay 2 vs 3 differ in namespaces and in how setTestMode behaves, and most Laravel wrappers (e.g. barryvdh/laravel-omnipay) only support one of them.src/Gateway.php (the setters it exposes) and src/Message/PurchaseRequest.php + PurchaseResponse.php (how it builds Ds_MerchantParameters and signs them). Then look at CompletePurchaseRequest — that is where callback verification lives.use Omnipay\Omnipay;
$gateway = Omnipay::create('Sermepa'); // short name defined in Gateway::getShortName()
$gateway->setMerchantCode(config('services.redsys.merchant_code')); // FUC
$gateway->setMerchantTerminal(config('services.redsys.terminal', '001'));
$gateway->setMerchantName(config('services.redsys.merchant_name'));
$gateway->setMerchantSecret(config('services.redsys.secret')); // clave secreta from the bank
$gateway->setCurrency('EUR');
$gateway->setTestMode(app()->environment('local', 'staging'));
$response = $gateway->purchase([
'amount' => '10.00',
'orderId' => $order->id, // MUST be unique, 4–12 chars
'description' => 'Order #'.$order->id,
'notifyUrl' => route('redsys.notify'), // server-to-server
'returnUrl' => route('redsys.return'), // browser
])->send();
if ($response->isRedirect()) {
return redirect($response->getRedirectUrl());
}
Then wire two routes. The notification route must be excluded from CSRF and auth middleware, because Redsys POSTs to it from outside your app:
// app/Http/Middleware/VerifyCsrfToken.php
protected $except = ['redsys/notify'];
Bind the gateway once. Create an App\Payments\RedsysGateway factory or bind Omnipay\Sermepa\Gateway as a singleton in a service provider. Do not scatter setMerchantSecret() across controllers.
Persist before redirecting. Create a payments row (order_id, amount, status = pending, gateway = redsys) before calling purchase(). The orderId you send to Redsys should be a value you can look up when the callback arrives — it is both your correlation key and the seed for the signature.
Server notification (the one that matters):
public function notify(Request $request)
{
$gateway = app(RedsysGateway::class);
$response = $gateway->completePurchase($request->all())->send();
$payment = Payment::where('gateway_order_id', $request->input('Ds_Order'))->firstOrFail();
// 1. verify signature (done inside completePurchase — check isSuccessful)
// 2. verify amount matches what you stored
// 3. idempotency: bail if already marked paid
if ($payment->status === 'paid') {
return response('OK', 200);
}
if ($response->isSuccessful() && $response->getAmount() == $payment->amount) {
$payment->markPaid($response->getTransactionReference()); // Ds_AuthorisationCode
// dispatch fulfilment job
} else {
$payment->markFailed($response->getMessage());
}
return response('OK', 200); // Redsys retries on non-200
}
Browser return: treat this as a UI hint only. Redirect the user to a "thank you / pending" page and let the notification be the source of truth. The return leg can be replayed or spoofed by the customer; never mark an order paid from it.
Refunds and pre-auth: $gateway->refund([...]) and $gateway->authorize()/capture() map to Redsys preauthorization and refund operations. These require the original Ds_Order and often the authorization code, so store Ds_AuthorisationCode, Ds_Response, and the signed parameters on your payment record when the notification arrives. Without them you cannot refund later.
Testing: Redsys provides a separate test merchant code/terminal and secret, plus the sis-t.redsys.es endpoint (setTestMode(true) should switch the URL — verify this in PurchaseRequest::getEndpoint()). You can exercise the notification handler by POSTing a crafted Ds_* payload to redsys/notify; keep a small fixture of a real test callback for regression tests.
Ds_Order as the key input, then HMACs the parameters. If you change or regenerate the order ID between building the request and verifying the callback, the signature breaks. Use a deterministic ID (zero-padded integer, or sprintf('%012d', $id)).'10.00'), but Redsys transmits integer cents (1000). Confirm which side the driver converts on. A wrong amount yields either a signature error or a €0.10 charge — test with a real €0.01–€0.50 transaction in the test environment.Ds_SignatureVersion = HMAC_SHA256_V1. An old driver that signs SHA1 will be rejected. Grep the source for sha1 and patch to hash_hmac('sha256', ...) if needed.Ds_Response codes: 0000–0099 are authorized; everything else is a decline (0101 expired card, 0190 denied, etc.). The driver usually maps this to isSuccessful(), but store the raw code anyway for support tickets.OK). If your app throws a 500, Redsys will retry for hours.orderId (e.g. append attempt counter) rather than retrying the same one.notifyUrl. A URL behind basic auth or an unvalidated certificate will silently fail. In local dev use a tunnel (ngrok/Expose) rather than a localhost URL.composer.json, copy the gateway into your app (or a fork) if you must keep it, and audit Gateway.php against the current Redsys docs. The alternative is to write a ~150-line signer/verifier yourself; many teams eventually do this because the signature logic is the only non-trivial part.Gateway::createRequest() decides which message class handles each operation, and the Message\* classes are plain and easy to subclass. If you need custom parameters (Ds_Merchant_ConsumerLanguage, Ds_Merchant_UrlOK, 3D Secure hints), override PurchaseRequest::getData() rather than monkey-patching the vendor directory.How can I help you explore Laravel packages today?