Gareth Wilson Gareth Wilson

Guide to Mollie Webhooks: Features and Best Practices

Published


Mollie webhooks tell your systems when something changes in your Mollie account: a payment is paid, fails or expires, a payment link is used, a sales invoice is settled, a payout lands. If you're running checkout, order fulfillment or finance reconciliation on Mollie, webhooks are how your backend learns about those changes without polling the API.

This guide covers Mollie's two webhook systems (classic and next-gen), how to secure each one, the platform's delivery limits, and the best practices for production.

What are Mollie webhooks?

Mollie webhooks are HTTP callbacks that notify your application when a resource changes state. Mollie runs two webhook systems side by side, and they work very differently.

Classic webhooks are attached to individual resources. You set a webhookUrl when you create a payment (or an order, subscription or payment link), and Mollie POSTs to it whenever that resource's status changes. The request carries no status, no event type and no signature: the body is a single form-encoded parameter, id=tr_5B8cwPMGnU6qLbRvo7qEZo. Your handler fetches the resource from the Mollie API to find out what happened.

Next-gen webhooks are account-level subscriptions. You register a URL once, choose the event types you want, and Mollie POSTs a JSON event to it. Each event has resource: "event", an event id, a type such as payment-link.paid, the entityId of the affected object, createdAt and _links. With the full payload option, the event also carries a snapshot of the resource under _embedded. Next-gen deliveries are signed with an X-Mollie-Signature header.

Mollie webhook features

FeatureDetails
ConfigurationClassic: webhookUrl set per resource via the API. Next-gen: Dashboard (Developers > Webhooks) or API (POST /v2/webhooks)
Event selectionClassic: none, every status change of the resource is sent. Next-gen: a list of eventTypes, or * for all
AuthenticationClassic: unsigned, confirm by fetching the resource. Next-gen: HMAC-SHA256 (hex) of the raw body in X-Mollie-Signature, formatted sha256=<signature>
PayloadClassic: application/x-www-form-urlencoded body with a single id. Next-gen: JSON event with type and entityId, plus an _embedded snapshot in full payload mode
Secret rotationNext-gen: two signature headers on every event for 24 hours after a rotation
Timeout15 seconds
RetriesUp to 10 attempts over roughly 26 hours, with increasing intervals; anything other than 200 OK counts as a failure
Failure handlingNext-gen: a webhook that keeps failing over 24 hours is marked blocked and delivery stops
TransportNext-gen live mode requires an HTTPS URL; test mode has no such requirement
LimitsNext-gen: 2 webhook subscriptions per organization in test mode, up to 3 in live mode

Common events

Next-gen event names are the type values. Classic webhooks have no event type at all.

EventFires when
payment.paidA payment succeeds
payment.authorizedFunds are reserved for a pay-later or two-step payment
payment.failedA payment attempt fails
payment.expiredA payment is not completed in time
payment.canceledA payment is canceled before completion
payment-link.paidA payment link is paid
sales-invoice.issuedA sales invoice is issued
sales-invoice.paidA sales invoice is paid
payout.completedA payout completes
payout.failedA payout fails
balance-transaction.createdA new transaction is recorded on your balance
refund.refundedA refund completes (beta)
dispute.createdA dispute is opened (beta)

For next-gen webhooks, branch on the top-level type field and use entityId to locate the object. For classic webhooks, fetch the resource by id and branch on its status (paid, authorized, failed, expired, canceled and so on).

Inspect Mollie webhooks as they arrive. Point Mollie at a Hookdeck Console URL to inspect and replay real deliveries — no account or setup required.

Setting up Mollie webhooks

For classic webhooks, there is no dashboard setting. You pass webhookUrl each time you create a payment, and Mollie calls it on every status change for that payment, including refunds and chargebacks:

const { createMollieClient } = require('@mollie/api-client');
const mollie = createMollieClient({ apiKey: process.env.MOLLIE_API_KEY });

const payment = await mollie.payments.create({
  amount: { currency: 'EUR', value: '10.00' },
  description: 'Order #12345',
  redirectUrl: 'https://your-app.com/order/12345',
  webhookUrl: 'https://your-app.com/webhooks/mollie',
  metadata: { order_id: '12345' },
});

For next-gen webhooks, create a subscription in the dashboard under Developers > Webhooks, or via the API. The call requires an access token with the webhooks.write scope:

curl -X POST https://api.mollie.com/v2/webhooks \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -d "name=Order events" \
  -d "url=https://your-app.com/webhooks/mollie/events" \
  -d "eventTypes=payment-link.paid"

Pass eventTypes as a list or * for every event, and testmode=true to create a test-mode subscription. The response includes a webhookSecret: store it, because it is the key you verify signatures with. Live-mode subscriptions need an HTTPS URL.

For local development, use the Hookdeck CLI: hookdeck listen 3000 mollie --path /webhooks/mollie gives you a public HTTPS URL that forwards to your local server, plus a web UI for inspecting and replaying deliveries, with no account required. Use the generated URL as the webhookUrl on test payments, or as the URL of a test-mode next-gen subscription.

Securing Mollie webhooks

The two systems are secured in different ways, so they need different handlers.

Next-gen webhooks carry X-Mollie-Signature: sha256=<signature>, where the signature is a hex-encoded HMAC-SHA256 of the unaltered request body, keyed with your webhook secret. Strip the sha256= prefix, compute the HMAC over the raw body, and compare with a timing-safe function. During a secret rotation the header is sent twice for 24 hours; Node joins duplicate headers into one comma-separated string, so accept the request if either value matches.

Classic webhooks have nothing to verify. Mollie's security model is fetch-to-confirm: read the id, fetch the payment with your API key, and act only on the status the API returns. A forged request can at most make you re-fetch a payment you already own.

const crypto = require('crypto');
const express = require('express');
const { createMollieClient } = require('@mollie/api-client');

const app = express();
const mollie = createMollieClient({ apiKey: process.env.MOLLIE_API_KEY });

function verifyMollieSignature(rawBody, header, secret) {
  if (!header) return false;
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');

  // During a rotation Mollie sends two headers: "sha256=<a>, sha256=<b>"
  return header.split(',').some((part) => {
    const signature = part.trim().replace(/^sha256=/, '');
    try {
      return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
    } catch {
      return false; // Buffers of different lengths throw
    }
  });
}

// Next-gen webhooks: signed JSON events
app.post('/webhooks/mollie/events', express.raw({ type: '*/*' }), (req, res) => {
  const isValid = verifyMollieSignature(
    req.body,
    req.headers['x-mollie-signature'],
    process.env.MOLLIE_WEBHOOK_SECRET
  );
  if (!isValid) {
    return res.status(400).send('Invalid signature');
  }

  const event = JSON.parse(req.body);
  switch (event.type) {
    case 'payment-link.paid':
      // Look up event.entityId, fulfill the order
      break;
    case 'payout.failed':
      // Alert finance
      break;
  }

  res.status(200).send('OK');
});

// Classic webhooks: unsigned, form-encoded, id only
app.post('/webhooks/mollie', express.urlencoded({ extended: false }), async (req, res) => {
  const id = req.body && req.body.id;
  if (!id) {
    return res.status(400).send('Missing id');
  }

  let payment;
  try {
    payment = await mollie.payments.get(id);
  } catch (err) {
    if (err && err.statusCode === 404) {
      return res.status(200).send('OK'); // Unknown id: acknowledge, nothing to do
    }
    return res.status(500).send('Could not fetch payment'); // Let Mollie retry
  }

  switch (payment.status) {
    case 'paid':
      // Fulfill the order
      break;
    case 'expired':
    case 'failed':
    case 'canceled':
      // Release reserved stock
      break;
  }

  res.status(200).send('OK');
});

Note the body parsers: next-gen signatures need the raw bytes (express.raw()), while classic webhooks are form-encoded (express.urlencoded()), so a JSON parser breaks both.

Make Mollie webhooks production-ready. Hookdeck Event Gateway verifies deliveries at the edge, deduplicates, and durably queues every event with replay for anything that fails.

Mollie webhook limitations and pain points

Classic webhooks carry no signature and no data

The Problem: A classic webhook body is id=tr_... and nothing else. There is no signature to check, and no status or event type to act on, so every delivery costs an API round trip before you know what happened. If the Mollie API is slow or unreachable at that moment, you cannot process the webhook at all.

Why It Happens: Mollie designed classic webhooks around fetch-to-confirm. Because the status is never sent, a forged request cannot mark an order as paid, so a signature was never needed. Mollie also advises against IP allowlisting, as its webhook source IPs change.

Workarounds:

  • Treat the body as a hint only and always act on the fetched status.
  • Return 500 when the fetch fails for a transient reason so Mollie retries, and 200 for a 404 so it stops.

How Hookdeck Can Help: Hookdeck cannot verify classic webhooks, since there is no signature to check, but it gives them a durable entry point. Every delivery is logged, queued and retried on your schedule, so a failed fetch on your side does not depend on Mollie's retry window, and you can replay any delivery once the Mollie API is reachable again.

Two webhook systems with different contracts

The Problem: Classic and next-gen webhooks differ in content type, payload, security and event coverage. Classic covers payments, orders, subscriptions and payment links per resource. Next-gen adds payouts, sales invoices, balance transactions and business account transfers, with refunds, chargebacks and disputes still in beta. Many integrations end up running both.

Why It Happens: Next-gen webhooks were introduced alongside the existing per-resource model rather than replacing it, so both remain in use.

Workarounds:

  • Give each system its own route and body parser, as in the example above.
  • Keep classic webhooks for resources that next-gen does not yet cover, and move the rest to subscriptions.

How Hookdeck Can Help: Create one Hookdeck source per system. Turn on HMAC verification for the next-gen source (Hookdeck's Mollie source type verifies X-Mollie-Signature) and leave it off for the classic source, since unsigned id=tr_... posts would fail verification. Then use filters on the next-gen type field to route event families to different destinations.

Failed next-gen webhooks get blocked

The Problem: Mollie waits 15 seconds for a response and retries up to 10 times over roughly 26 hours. If a next-gen webhook keeps failing across that period, Mollie marks it blocked and stops delivering to it entirely.

Why It Happens: Mollie expects 200 OK quickly and treats slow or non-200 responses as failures. Repeated failures are taken as a sign the endpoint is gone.

Workarounds:

  • Return 200 as soon as the signature checks out, and do the work in a background job.
  • Monitor the webhook's status in the dashboard or via the API, and re-enable it after an incident.

How Hookdeck Can Help: Hookdeck accepts each delivery and responds immediately, so Mollie sees a healthy endpoint even while your service is down. Events wait in a durable queue, retry against your service on your own schedule, and Issues alert you to failures, with bulk replay once the fix is deployed.

Secret rotation sends two signatures

The Problem: When you rotate a next-gen webhook secret, every event carries two signature headers for 24 hours. A handler that treats the header as a single value fails verification for the whole rotation window.

Why It Happens: Mollie signs with both the old and new secrets during rotation so you can switch your stored secret without dropping events. Most frameworks join repeated headers into one comma-separated value.

Workarounds:

  • Split the header on commas and accept the request if any value matches.
  • Update the stored secret within the 24-hour window, after which the old one stops working.

How Hookdeck Can Help: Hookdeck's HMAC verification splits the combined sha256=<a>, sha256=<b> header, so verification keeps passing through the rotation and you only update the secret in one place.

Best practices

Fetch to confirm on classic webhooks

Never take a classic webhook body as proof of anything. Fetch the payment with your API key, read status, and act on that. Return 200 for ids Mollie doesn't recognize, so unknown or deleted ids don't trigger a day of retries.

Verify next-gen signatures against the raw body

Compute the HMAC over the unaltered body with .digest('hex'), strip the sha256= prefix from the header, compare with crypto.timingSafeEqual, and handle the two-header case during rotation.

Acknowledge fast, process asynchronously

With a 15-second timeout and a blocked state for persistently failing next-gen webhooks, slow handlers carry real risk. Return 200 once the request is accepted and move fulfillment, emails and ledger updates to a queue. See why to process webhooks asynchronously.

Make handlers idempotent

Mollie may call your webhook more than once for the same status, and every retry is another delivery. Key next-gen processing on the event id, and make classic status handling safe to repeat so a second paid never ships an order twice. Don't deduplicate classic webhooks on the body: every status change for a payment sends the identical id=tr_.... See our guide to webhook idempotency.

Test with test mode

Use a test_ API key and Mollie's test checkout to drive payments into each status, and a test-mode next-gen subscription to receive events without an HTTPS requirement. Switching to live changes the key and secret, not the handler code.

Conclusion

Mollie gives you two ways to receive webhooks. Classic webhooks attach to each payment, send only an id, and rely on you fetching the resource to learn its status. Next-gen webhooks are account-level subscriptions that send typed JSON events signed with HMAC-SHA256 in X-Mollie-Signature. Most production integrations need to handle both, each with its own parser and its own security check.

Delivery stops at 10 attempts over about 26 hours, and a next-gen webhook that keeps failing is blocked, so an endpoint that acknowledges quickly and processes in the background protects you from losing events. Hookdeck Event Gateway verifies next-gen signatures, queues classic and next-gen deliveries durably, and lets you replay anything that fails, so your handlers stay small and your event feed stays on.

Get started with Hookdeck for free and handle Mollie webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.