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

Mollie Laravel Package

omnipay/mollie

Omnipay Mollie is a Mollie gateway driver for the Omnipay PHP payment library. It lets Laravel and PHP apps create payments, handle redirects and webhooks, and process refunds through a consistent API, simplifying Mollie integration into existing Omnipay checkout flows.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require omnipay/mollie
# if you don't already have a PSR-18/PSR-7 implementation registered:
composer require guzzlehttp/guzzle php-http/guzzle7-adapter

The driver sits on top of omnipay/common (v3 recommended) and, in current versions, the official mollie/mollie-api-php client. Check composer.json in the package once to confirm which HTTP stack it pulls in — it determines whether you debug via Omnipay's getHttpClient() or via $gateway->getApi().

Configure

// config/services.php
'mollie' => [
    'key' => env('MOLLIE_API_KEY'),   // test_xxxxxxxx or live_xxxxxxxx
],
// app/Providers/AppServiceProvider.php
$this->app->singleton(\Omnipay\Mollie\Gateway::class, function () {
    return \Omnipay\Omnipay::create('Mollie')
        ->setApiKey(config('services.mollie.key'));
});

First use case — a redirect (iDEAL / card / Bancontact) payment

$gateway = app(\Omnipay\Mollie\Gateway::class);

$response = $gateway->purchase([
    'amount'      => '10.00',          // major units, string
    'currency'    => 'EUR',
    'description' => 'Order #1042',
    'returnUrl'   => route('checkout.return', $order),
    'notifyUrl'   => route('webhooks.mollie'),
    'paymentMethod' => 'ideal',
    'issuer'      => $request->input('issuer'), // optional, from fetchIssuers
    'metadata'    => ['order_id' => $order->id],
])->send();

if ($response->isRedirect()) {
    $order->update(['mollie_payment_id' => $response->getTransactionReference()]);
    return $response->redirect();       // or redirect()->away($response->getRedirectUrl())
}

throw new \RuntimeException($response->getMessage());

Where to look first

  • src/Gateway.php — the supported operations (purchase, completePurchase, fetchTransaction, refund, and whether customer/mandate requests exist in your version).
  • src/Message/*Request.php — the exact parameter names each request reads. This is the only reliable source of truth for paymentMethod, issuer, metadata, cardToken, etc.
  • The Mollie dashboard — every payment you create appears there with the raw status and the request/response log.

Implementation Patterns

1. Two-phase checkout (create → return → verify)

Never trust the return URL alone. The redirect back to your app only carries ?id=tr_xxxxx; the truth is on Mollie's side.

public function return(Request $request, Order $order)
{
    $response = app(Gateway::class)->completePurchase([
        'transactionReference' => $request->query('id'),
    ])->send();

    $data = $response->getData();

    abort_unless($data['id'] === $order->mollie_payment_id, 403);

    if (($data['status'] ?? null) === 'paid'
        && $data['amount']['value'] === $order->amount
        && $data['amount']['currency'] === $order->currency) {
        $order->markPaid();
    }

    return redirect()->route('orders.show', $order);
}

completePurchase is effectively fetchTransaction plus status interpretation — it is a safe, idempotent read; call it as often as you need.

2. Webhook handler (the authoritative path)

// routes/web.php — outside the "web" middleware group, or exclude from CSRF
Route::post('/webhooks/mollie', MollieWebhookController::class)
    ->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken::class]);
public function __invoke(Request $request)
{
    $id = $request->input('id');            // Mollie posts form-encoded `id`

    $response = app(Gateway::class)
        ->fetchTransaction(['transactionReference' => $id])
        ->send();

    if (! $response->isSuccessful()) {
        return response('not found', 404);
    }

    $payment = $response->getData();

    ProcessMolliePayment::dispatch($payment); // queued, idempotent
    return response('ok', 200);               // anything 2xx stops retries
}

Make ProcessMolliePayment idempotent (guard on status transitions, key on $payment['id']). Mollie retries non-2xx responses, so a naive handler will double-credit orders.

3. Refunds

$refund = app(Gateway::class)->refund([
    'transactionReference' => $order->mollie_payment_id,
    'amount'               => '10.00',   // omit for a full refund
    'description'          => 'Customer return',
])->send();

if (! $refund->isSuccessful()) {
    Log::error('Mollie refund failed', ['msg' => $refund->getMessage(), 'data' => $refund->getData()]);
}

Store the refund's own ID ($refund->getTransactionReference()) — you'll need it if you later want to fetchRefund or reconcile.

4. Wrapping the gateway in an application service

Avoid scattering $gateway->purchase([...])->send() through controllers. A thin MollieService (or a single CreateMolliePayment action) keeps parameter names, amount formatting, and the metadata contract in one place and makes it trivial to swap to the official SDK one day.

final class StartMolliePayment
{
    public function __construct(private Gateway $gateway) {}

    public function __invoke(Order $order, string $method, ?string $issuer = null)
    {
        return $this->gateway->purchase(array_filter([
            'amount'        => number_format($order->total, 2, '.', ''),
            'currency'      => 'EUR',
            'description'   => "Order {$order->reference}",
            'returnUrl'     => route('checkout.return', $order),
            'notifyUrl'     => route('webhooks.mollie'),
            'paymentMethod' => $method,
            'issuer'        => $issuer,
            'metadata'      => ['order_id' => $order->id],
        ]))->send();
    }
}

5. Issuer selection for iDEAL

iDEAL needs an issuer ID. If your version exposes fetchIssuers, use it and cache the result (the list changes rarely). If not, read GET /v2/methods?include=issuers from Mollie directly and feed the chosen ID back into purchase(['issuer' => ...]).

6. Recurring / subscriptions

Omnipay's model is one-shot. If the driver in your version exposes createCustomer/createMandate, use them for SEPA direct debit; for Mollie's newer Subscriptions API you'll usually be better served going straight to mollie/mollie-api-php and keeping the Omnipay gateway only for the initial payment.

7. Reconciliation job

Schedule a nightly command that fetches any order in pending/open older than a few hours and re-runs fetchTransaction. Webhooks occasionally get lost during deploys or DNS blips; a sweep closes those gaps.


Gotchas and Tips

Amounts are strings in major units. Pass '10.00', not 1000 and not 10.0 or a float. Passing a float can produce 10.0 in the API payload and Mollie rejects malformed amounts. Format with number_format($total, 2, '.', '').

transactionReference is the Mollie payment ID (tr_...). getTransactionId() may return the same value or be empty depending on version — read getTransactionReference() and also keep $response->getData()['id'] as a sanity check.

No setTestMode(). Test vs. live is decided entirely by the API key prefix (test_ / live_). Don't try to toggle it; it silently does nothing.

Webhooks are unsigned. Mollie does not sign the callback. The only safe verification is fetching the payment and comparing id, amount.value, and amount.currency against your stored order. Treat the webhook body as a hint, nothing more.

status is not boolean. The lifecycle is open → pending/authorized → paid, with terminal failures failed, canceled, expired, refunded, charged_back. isSuccessful() on a fetch response generally means "the API call worked", not "the customer paid". Always branch on $data['status'] === 'paid'.

Currency is method-constrained. iDEAL, Bancontact, and several others are EUR-only. Sending USD with paymentMethod => 'ideal' fails at Mollie, not in the driver — you'll get a response error, not an exception.

notifyUrl requirements. Must be publicly reachable over HTTPS, must return 2xx, and should not sit behind auth or CSRF. Mollie appends nothing — the payment ID arrives as the form field id in the POST body.

Mollie retries webhooks. For up to ~a day, on any non-2xx or timeout. Your handler must be idempotent and fast; dispatch a queued job and return 200 immediately.

metadata round-trips. It comes back on the payment object, so you can recover the order in the webhook without a lookup table — but keep it small, JSON-serializable, and never put PII or secrets in it (it's visible in the dashboard).

Card payments need a token. You cannot post raw PAN data; Mollie requires a card token minted by Mollie Components. Look for cardToken/card handling in PurchaseRequest.php; if it's absent, the driver only supports redirect-style methods and you'll need Components on the frontend.

Refund limits. Partial refunds cannot exceed the remaining refundable amount on the payment. Track refunded totals yourself; a second refund attempt fails.

Settlement ≠ payment amount. For some methods the settlementAmount differs from amount due to fees. Don't reconcile payouts against the payment amount.

Error handling. send() may throw InvalidResponseException on transport/HTTP failures (depending on omnipay/common version and the HTTP client). Wrap calls in try/catch and check isSuccessful():

try {
    $response = $gateway->purchase($params)->send();
} catch (\Omnipay\Common\Exception\OmnipayException $e) {
    report($e);
    return back()->withErrors('Payment provider unavailable.');
}

if (! $response->isRedirect() && ! $response->isSuccessful()) {
    Log::warning('Mollie', ['message' => $response->getMessage(), 'data' => $response->getData()]);
}

Log getData() in non-production. It contains the full decoded Mollie response — the fastest way to debug a failing iDEAL or an unexpected status.

Test-mode payments don't auto-settle. On a test_ key, iDEAL/card payments sit at open until you flip the status in the dashboard (or Mollie's test controls). Integration tests should stub the gateway rather than await a webhook.

Version drift. The package has had breaking changes around customer/mandate support and around whether it proxies through the official SDK. Pin a major version in composer.json and read CHANGELOG.md before upgrading.

Extension point. For anything the driver doesn't cover (subscriptions, payouts, Idempotency-Key headers), reach for $gateway->getApi() if it exists, or inject \Mollie\Api\MollieApiClient alongside the Omnipay gateway. Mixing both in one service is normal and perfectly fine.

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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package