Fintech

Mollie API Integration Patterns for Production Applications

Production-tested patterns for integrating the Mollie Payments API, covering payments, refunds, webhooks, and error handling.

Working With Mollie in Production

Mollie is one of the most developer-friendly payment service providers in Europe. Their API is clean, their documentation is solid, and their PHP SDK is well-maintained. But moving from sandbox to production reveals subtleties that the quick start guide does not cover.

This article shares patterns refined over years of running Mollie integrations in production Laravel applications.

Payment Creation Patterns

Basic Payment With Metadata

Always attach metadata to your payments. When a webhook arrives, you need to know which order or subscription triggered it:

$payment = $mollie->payments->create([
    'amount' => [
        'currency' => 'EUR',
        'value' => '29.95',
    ],
    'description' => "Order #{$order->number}",
    'redirectUrl' => route('orders.thank-you', $order),
    'webhookUrl' => route('webhooks.mollie'),
    'metadata' => [
        'order_id' => $order->id,
        'type' => 'order_payment',
    ],
]);

Store the Mollie payment ID on your local record immediately after creation. You need this to correlate webhooks and API lookups:

$order->update([
    'mollie_payment_id' => $payment->id,
    'payment_status' => 'pending',
]);

Method-Specific Parameters

Different payment methods accept different parameters. Structure your code to handle this:

$params = [
    'amount' => $amount,
    'description' => $description,
    'redirectUrl' => $redirectUrl,
    'webhookUrl' => $webhookUrl,
    'metadata' => $metadata,
];

if ($method === 'ideal') {
    $params['method'] = 'ideal';
    $params['issuer'] = $request->input('issuer');
}

if ($method === 'creditcard') {
    $params['method'] = 'creditcard';
    // SCA is handled by Mollie's hosted checkout
}

if ($method === 'bancontact') {
    $params['method'] = 'bancontact';
}

Pre-Selecting Payment Methods

You can let Mollie's hosted payment page handle method selection, or pre-select a method. Pre-selection improves UX by reducing steps, but means you need to handle method-specific flows in your frontend.

Use Mollie's Methods API to fetch available methods with correct pricing for the transaction:

$methods = $mollie->methods->allActive([
    'amount' => ['value' => '29.95', 'currency' => 'EUR'],
    'resource' => 'payments',
]);

Cache this response (methods change infrequently) but invalidate the cache daily.

Webhook Processing

The Webhook Controller

Mollie webhooks send only the payment ID. You must fetch the full payment details yourself:

class MollieWebhookController extends Controller
{
    public function __invoke(Request $request)
    {
        // Mollie sends the payment ID in the request body
        $paymentId = $request->input('id');

        // Dispatch to a queued job for processing
        ProcessMollieWebhook::dispatch($paymentId);

        return response('', 200);
    }
}

The Processing Job

The queued job fetches the payment and updates your records:

class ProcessMollieWebhook implements ShouldQueue
{
    use InteractsWithQueue, SerializesModels;

    public int $tries = 5;
    public array $backoff = [30, 60, 300, 900, 3600];

    public function __construct(
        private string $paymentId,
    ) {}

    public function handle(MollieApiClient $mollie): void
    {
        $payment = $mollie->payments->get($this->paymentId);

        $order = Order::where('mollie_payment_id', $payment->id)->first();

        if (! $order) {
            Log::warning("Webhook for unknown payment: {$payment->id}");
            return;
        }

        match ($payment->status) {
            'paid' => $this->handlePaid($order, $payment),
            'failed' => $this->handleFailed($order),
            'expired' => $this->handleExpired($order),
            'canceled' => $this->handleCanceled($order),
            default => null,
        };
    }
}

Why fetch instead of trusting the webhook payload? Mollie's webhook only contains the ID. Even if it contained full payment data, you should always fetch the authoritative state from Mollie's API. This prevents spoofed webhooks from corrupting your data.

Idempotent Processing

Webhooks may arrive multiple times. Protect against duplicate processing:

private function handlePaid(Order $order, Payment $payment): void
{
    if ($order->payment_status === 'paid') {
        return; // Already processed
    }

    DB::transaction(function () use ($order, $payment) {
        $order->update([
            'payment_status' => 'paid',
            'paid_at' => $payment->paidAt,
        ]);

        $order->fulfill();
    });
}

Refund Handling

Creating Refunds

$refund = $mollie->paymentRefunds->createForId($paymentId, [
    'amount' => [
        'currency' => 'EUR',
        'value' => '10.00',
    ],
    'description' => "Partial refund for order #{$order->number}",
]);

Track refunds locally. Create a refund record linked to both the order and the original payment. You need this for reconciliation and customer support.

Refund Webhooks

Mollie sends a webhook when a refund status changes. The same payment webhook endpoint receives these notifications. When you fetch the payment, check its refunds:

$payment = $mollie->payments->get($paymentId, ['include' => 'refunds']);

foreach ($payment->refunds() as $refund) {
    // Update local refund records
}

Error Handling Patterns

API Errors

Wrap Mollie API calls in try-catch blocks that distinguish between retryable and permanent errors:

try {
    $payment = $mollie->payments->create($params);
} catch (ApiException $e) {
    if ($e->getCode() >= 500) {
        // Mollie server error, retry later
        throw $e; // Let the queue retry
    }

    if ($e->getCode() === 422) {
        // Validation error, do not retry
        Log::error("Mollie validation error", [
            'params' => $params,
            'error' => $e->getMessage(),
        ]);
        throw new PaymentCreationFailed($e->getMessage());
    }
}

Rate Limiting

Mollie's API has rate limits. In high-volume scenarios:

  • Implement exponential backoff on 429 responses
  • Batch operations where possible (use the Orders API for multi-item transactions)
  • Cache responses that do not change frequently (methods, profiles)

Testing With Mollie

Sandbox Environment

Mollie's test mode uses separate API keys. Configure per environment:

// config/services.php
'mollie' => [
    'key' => env('MOLLIE_KEY'),
],

Use test API keys in development and staging. Test mode supports simulated payment methods that complete instantly.

Testing Webhooks Locally

During development, Mollie cannot reach your localhost. Options:

  • Use ngrok or expose to tunnel your local server
  • Use Mollie's webhook testing feature in their dashboard
  • Write integration tests that call your webhook endpoint directly with known payment IDs

Mollie's API is well designed, but production usage demands attention to webhook reliability, idempotency, and error handling. Invest in these patterns early. They prevent the kind of payment discrepancies that erode customer trust and create accounting nightmares.

Let's talk about your fintech needs

Whether you're modernizing your infrastructure, navigating compliance, or building new software - we can help.

Book a 30-min Call