Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Sermepa Omnipay Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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.

  1. Install — 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.
  2. Read the source first — there is effectively no documentation surface for a package this small. Start with 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.
  3. Configure via a service provider or a small factory rather than passing secrets at call sites:
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'));
  1. First use case — a redirect purchase. Almost every Redsys integration is: create order → redirect to bank → receive server-to-server notification → receive browser return.
$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'];

Implementation Patterns

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.

Gotchas and Tips

  • The order number is the signing key seed. Redsys derives a per-order key via 3DES using 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)).
  • Amount units are the #1 failure mode. Omnipay convention is a decimal string ('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.
  • SHA1 vs SHA256. Redsys deprecated SHA1 years ago; the request must use 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.
  • The merchant secret is not hex — it is the string the bank gives you (often looks base64-ish). Do not hex-decode or base64-decode it before use.
  • 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.
  • Redsys retries notifications. Your handler must be idempotent and return HTTP 200 with a short body (OK). If your app throws a 500, Redsys will retry for hours.
  • Duplicate order IDs are rejected. After a failed/cancelled attempt, generate a fresh orderId (e.g. append attempt counter) rather than retrying the same one.
  • HTTPS, public, no query strings on 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.
  • Compatibility risk is real. The package has near-zero adoption (0 stars/devs) and appears unmaintained. Pin the version in 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.
  • Where to extend: 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.
  • Log the raw payload and your computed signature (masked) on failures. Redsys's error responses are terse — typically just an error code — and the difference between "bad signature" and "unknown merchant" is entirely in what you sent.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel