mollie/mollie-api-php
Mollie API client for PHP: accept iDEAL, Apple Pay, credit card, SEPA, PayPal, Klarna and more with a clean, well-tested library. Create payments, handle refunds, subscriptions, customers, and webhooks through a simple, intuitive API.
1. Install and wire it up. The package is framework-agnostic, so there is no service provider to publish — you bind it yourself.
composer require mollie/mollie-api-php
// .env
MOLLIE_KEY=test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
// app/Providers/AppServiceProvider.php
use Mollie\Api\MollieApiClient;
public function register(): void
{
$this->app->singleton(MollieApiClient::class, fn () => (new MollieApiClient())
->setToken(config('services.mollie.key')));
}
Add 'mollie' => ['key' => env('MOLLIE_KEY')] to config/services.php. setToken() auto-detects test_/live_ API keys and access_ OAuth tokens; use setApiKey() / setAccessToken() if you prefer to be explicit.
2. Where to look first. In order:
docs/requests.md — the v3 API is request-object based, not array-based. Understand this before anything else.docs/recipes/payments/create-payment.md — the canonical happy path.docs/webhooks.md + docs/recipes/payments/handle-webhook.md — this is where most production bugs live.docs/testing.md and docs/debugging.md — read before you write your first test, not after.3. First use case: a redirect checkout. Create the payment on your own server, then send the customer to Mollie.
use Mollie\Api\Http\Data\Money;
use Mollie\Api\Http\Requests\CreatePaymentRequest;
public function store(CheckoutRequest $request)
{
$order = Order::create($request->validated());
$payment = app(MollieApiClient::class)->send(new CreatePaymentRequest(
description: "Order #{$order->id}",
amount: new Money('EUR', number_format($order->total_cents / 100, 2, '.', '')),
redirectUrl: route('checkout.return', $order),
webhookUrl: route('webhooks.mollie'),
metadata: ['order_id' => $order->id],
));
$order->update(['mollie_payment_id' => $payment->id]);
return redirect($payment->getCheckoutUrl());
}
Get to a working test-mode payment in under an hour, then stop and design the webhook handler properly. The redirect is for the user's eyes; the webhook is the source of truth.
Request objects, not magic properties. v3 replaced $mollie->payments->create([...]) with typed requests in Mollie\Api\Http\Requests\ and value objects in Mollie\Api\Http\Data\ (Money, etc.). The benefit is static analysis: named arguments, IDE completion, and Money preventing float arithmetic on amounts. Treat the request classes as your DTO layer — do not wrap them in another array abstraction.
Webhook controller shape. Mollie POSTs application/x-www-form-urlencoded with just an id. Never trust it.
// bootstrap/app.php — or VerifyCsrfToken::$except
->withMiddleware(fn ($m) => $m->validateCsrfTokens(except: ['webhooks/mollie']))
public function __invoke(Request $request)
{
$payment = app(MollieApiClient::class)->send(
new GetPaymentRequest($request->input('id'))
);
ProcessMolliePayment::dispatch($payment->id)->onQueue('payments');
return response()->noContent(); // fast 2xx; do work in the queue
}
Rules that hold for every webhook handler: re-fetch the resource by ID, scope the lookup to the tenant/order you already have in your database, and make the handler idempotent (Mollie retries; the same event can arrive twice). Fire a domain event (PaymentPaid, PaymentFailed) from the job so the rest of the app never imports Mollie classes.
Status handling. Map paid → fulfil, failed/expired/canceled → release stock, authorized → capture later, pending → wait. Store the status you last acted on so a duplicate webhook is a no-op. For the customer returning from the redirect before the webhook lands, show a pending page and poll your own DB — do not call Mollie from the browser redirect handler and assume finality.
Money and the rest of the catalogue. Refunds (RefundPaymentRequest), captures for Klarna/credit card, chargebacks, customers, mandates, and subscriptions all follow the same request/response shape. Follow-up navigation on hydrated resources ($payment->refunds()) fires real HTTP calls — in a loop over 100 payments that is 100 requests, so prefer listing endpoints with pagination.
Idempotency. The client supports idempotency keys (see docs/idempotency.md). Derive the key from your own data — "order-{$order->id}-create" — so a queued retry or a double-clicked submit cannot produce two payments. This is the single cheapest guard against duplicate charges.
HTTP adapter for observability. docs/http-adapters.md lets you swap the transport for a PSR-18 client. Wire it to an instrumented Guzzle/Laravel client if you want structured logs, timeouts, and metrics on every Mollie call without editing call sites. Keep the timeout short — a payment page should not hang for 30s.
Testing. docs/testing.md covers MockResponse and faking responses; this is the right way to test your webhook handler and refund logic without network access. In feature tests, bind a configured fake MollieApiClient in the container and assert on the requests your code would send. Reserve real test-mode calls for a small smoke suite — test_ keys are free but the network is not deterministic in CI.
Mollie Connect / multi-tenant. If each tenant has their own Mollie account, install mollie/oauth2-mollie-php alongside and store an access_ token per organisation. Do not share a single connector across tenants; resolve per-request or per-job.
This is not a Laravel package. No facade, no auto-discovered provider, no config publish, no Mollie::fake() helper. Everything above you write once in a provider. The upside: no framework lock-in and no upgrade treadmill with Laravel majors.
v3 is a breaking rewrite of call style. Almost every blog post and StackOverflow answer you'll find uses $mollie->payments->create([...]). That's v2. Read UPGRADING.md before copying anything from the internet, and be aware $metadata handling changed (called out in v3.0.3).
getResponse() can be null. As of v3.11, webhook-hydrated resources have no HTTP Response attached. $payment->getResponse()->successful() will blow up in a webhook path. Null-check, or better: don't reach for the response at all — you already know you got a resource.
Webhook signatures are a newer addition; verification-by-refetch is still the contract. WebhookSnapshotOrigin / WebhookEventMapper::processPayload($payload, $signature) gives you event IDs and signed snapshots if you're on a recent version, but the classic HTTP webhook is unauthenticated by design. Fetching the payment by ID through the API is the authentication step, because Mollie is the only one who could have created that payment. Don't skip it.
setToken() will happily accept a live_ key in a test environment. Nothing stops you. Add an explicit guard — abort_unless(app()->environment('production') || str_starts_with($key, 'test_'), 500) — because the failure mode is real money.
Test mode is per-request for some endpoints. The testmode flag moved into the payload (v3.0.6); if you're sharing one live connector across test and real traffic in a staging environment, set it deliberately rather than relying on the key prefix.
Amounts are strings, not floats. new Money('EUR', '10.00'). Formatting 19.999 or sending 19.99 as a float through JSON has bitten people on rounding and on locale (comma decimal separators). Store cents as integers in your DB, convert once at the boundary, and always send the currency uppercase.
422 errors carry per-field detail — and some fields can be null. Catch Mollie\Api\Exceptions\ApiException; a 422 means your request was malformed (bad IBAN consumer name, unsupported method for the amount, missing locale for Klarna) and is a bug, not a transient failure. Handle 429 with backoff and 5xx with a retry that reuses the same idempotency key.
Configure the webhook route outside auth and outside CSRF. Also make sure it's reachable over public HTTPS — Mollie will not call http://localhost or a signed/staging URL. Use a tunnel in development; do not disable the webhook URL in test mode and "test the full flow later".
Return quickly. Mollie's timeout is short and it retries non-2xx. Dispatch a job and return 204. A slow webhook handler that does fulfilment inline will get retried and double-fulfil your order.
Don't log the connector. The debugging guide covers this, but the short version: log the request ID and the payment ID, never the Authorization header or a serialised request object that contains the key. MockResponse becoming serializable (v3.0.3) makes it tempting to dump responses into failed-job payloads — strip the connector first.
Partial refunds need a sum, not a boolean. Track cumulative refunded amount per payment in your DB and refuse refunds that exceed the captured total before you call the API; Mollie will reject it, but the error arrives after you've shown a success toast.
How can I help you explore Laravel packages today?