·14 min read·mollie, mollie-webhook, payment-webhook, webhook-testing, hookray

Mollie Webhooks: Setup, Testing & Payment Status Guide (2026)

Mollie webhooks notify your application the moment a payment status changes. A customer completes a checkout, a payment fails, a refund is issued — instead of polling the Mollie API every few seconds, your server receives an HTTP POST with the payment ID, and you fetch the latest status. No wasted API calls. No stale data. Your order management system, subscription engine, or accounting integration stays in sync automatically.

This guide covers everything you need to set up, test, and debug Mollie webhooks in production. Whether you're building an e-commerce checkout, a SaaS billing flow, or a marketplace payout system, this is the reference that will save you hours of trial and error.

What Are Mollie Webhooks and How They Work

A Mollie webhook is an HTTP callback that Mollie sends to a URL you specify whenever a payment or related object changes status. Unlike most webhook providers that send the full event payload in the request body, Mollie takes a different approach: the webhook request body contains only the object ID (e.g., tr_7UhSN1zuXS). Your server then calls the Mollie API to fetch the current status.

This design is intentional. By requiring your server to fetch the latest state from the API, Mollie ensures you always work with the most current data — not a snapshot that could be outdated by the time you process it.

Here's the flow:

  1. A customer initiates a payment through your checkout
  2. The payment status changes (e.g., from open to paid)
  3. Mollie sends an HTTP POST to your webhook URL with id=tr_7UhSN1zuXS in the body
  4. Your server receives the webhook and calls GET /v2/payments/tr_7UhSN1zuXS
  5. You read the current status field and update your order accordingly

This pattern applies to all Mollie webhook events — payments, refunds, chargebacks, subscriptions, and orders.

Why Mollie Webhooks Matter for Your Business

Without webhooks, you'd need to poll the Mollie API repeatedly to check payment statuses. For a store processing 500 orders per day, that means thousands of unnecessary API calls. With Mollie webhooks, you process exactly the events that happen, when they happen:

  • Instant order confirmation — Fulfill orders the second payment is confirmed, not minutes later
  • Automated subscription management — React to failed recurring payments immediately and trigger dunning flows
  • Real-time refund processing — Update your accounting system the moment a refund is issued
  • Chargeback alerts — Get notified of disputes before they escalate
  • Marketplace payouts — Track settlement status for multi-party payment flows

How to Configure Mollie Webhooks

You can set up Mollie webhooks in two ways: through the Mollie Dashboard for account-wide settings, or via the API for per-payment webhook URLs.

Method 1: Mollie Dashboard (Account-Level)

The dashboard approach sets a default webhook URL that applies to all payments created under your account.

  1. Log in to the Mollie Dashboard
  2. Navigate to Developers in the left sidebar
  3. Click Webhooks
  4. Enter your webhook endpoint URL (must be HTTPS in production)
  5. Click Save

This URL will be used for all payment status changes unless you override it at the API level.

Method 2: Mollie API (Per-Payment)

For more control, you can specify a webhook URL when creating each payment. This is the most common approach because it lets you route different payment types to different endpoints.

const { createMollieClient } = require('@mollie/api-client');
const mollieClient = createMollieClient({ apiKey: 'test_xxxxxxxxxxxxxxxxxxxxxxxx' });

const payment = await mollieClient.payments.create({
  amount: {
    currency: 'EUR',
    value: '49.99'
  },
  description: 'Annual Pro Plan',
  redirectUrl: 'https://yourapp.com/order/success',
  webhookUrl: 'https://yourapp.com/api/webhooks/mollie',
  metadata: {
    orderId: 'ORD-2026-1847',
    plan: 'pro-annual'
  }
});

You can also set the webhook URL when creating orders, subscriptions, and refunds:

// Setting webhookUrl on an order
const order = await mollieClient.orders.create({
  amount: { currency: 'EUR', value: '125.00' },
  orderNumber: 'ORD-2026-1848',
  webhookUrl: 'https://yourapp.com/api/webhooks/mollie-orders',
  lines: [
    {
      name: 'Wireless Keyboard',
      quantity: 1,
      unitPrice: { currency: 'EUR', value: '75.00' },
      totalAmount: { currency: 'EUR', value: '75.00' },
      vatRate: '21.00',
      vatAmount: { currency: 'EUR', value: '13.02' }
    },
    {
      name: 'USB-C Hub',
      quantity: 1,
      unitPrice: { currency: 'EUR', value: '50.00' },
      totalAmount: { currency: 'EUR', value: '50.00' },
      vatRate: '21.00',
      vatAmount: { currency: 'EUR', value: '8.68' }
    }
  ],
  billingAddress: {
    givenName: 'Jan',
    familyName: 'de Vries',
    email: 'jan@example.com',
    streetAndNumber: 'Keizersgracht 313',
    postalCode: '1016 EE',
    city: 'Amsterdam',
    country: 'NL'
  },
  redirectUrl: 'https://yourapp.com/order/success'
});

The per-payment webhook URL always takes priority over the dashboard default.

Mollie Webhook Events

Mollie webhooks fire whenever the status of a payment, refund, chargeback, subscription, or order changes. Here are the key status transitions you need to handle:

Payment Status Changes

StatusMeaningTypical Action
openPayment created, customer hasn't completed it yetShow "awaiting payment" in your UI
pendingPayment initiated but not yet confirmed (bank transfers, SOFORT)Show "processing" status
authorizedPayment authorized but not yet captured (credit cards with manual capture)Capture the payment or cancel
paidPayment successfully completedFulfill the order, send confirmation email
failedPayment failed (insufficient funds, 3DS failure, etc.)Show error, offer retry
canceledCustomer or merchant canceled the paymentRelease reserved inventory
expiredPayment expired before completionClean up the order, notify customer

Refund Status Changes

StatusMeaningTypical Action
queuedRefund is queued for processingUpdate order status to "refund pending"
pendingRefund is being processed by the bankNo action needed, wait for completion
refundedRefund successfully completedUpdate accounting, notify customer
failedRefund failedInvestigate and retry or process manually

Other Webhook Triggers

  • Chargebacks — When a customer disputes a payment with their bank
  • Subscriptions — When a recurring payment succeeds, fails, or is canceled
  • Settlements — When funds are settled to your bank account (Connect/marketplace)
  • Orders — When order status changes (authorized, paid, shipping, completed, etc.)

Sample Mollie Webhook Payload and Handling

When a Mollie webhook fires, the request is deliberately simple. Here's exactly what your server receives:

Incoming Webhook Request

POST /api/webhooks/mollie HTTP/1.1
Host: yourapp.com
Content-Type: application/x-www-form-urlencoded
User-Agent: Mollie HTTP

id=tr_7UhSN1zuXS

That's it. The body contains only id=tr_7UhSN1zuXS as a form-encoded parameter. No JSON payload. No event type. No status. You must call the Mollie API to get the details.

Fetching the Payment Status

Here's a complete webhook handler in Node.js/Express:

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

const app = express();
app.use(express.urlencoded({ extended: true }));

const mollieClient = createMollieClient({
  apiKey: process.env.MOLLIE_API_KEY
});

app.post('/api/webhooks/mollie', async (req, res) => {
  // Always respond 200 immediately — Mollie retries on non-2xx
  res.status(200).send('OK');

  const paymentId = req.body.id;
  if (!paymentId) return;

  try {
    const payment = await mollieClient.payments.get(paymentId);

    console.log(`Payment ${paymentId} status: ${payment.status}`);

    switch (payment.status) {
      case 'paid':
        await fulfillOrder(payment.metadata.orderId);
        await sendConfirmationEmail(payment.metadata.orderId);
        break;

      case 'failed':
        await markOrderFailed(payment.metadata.orderId);
        await sendPaymentFailedEmail(payment.metadata.orderId);
        break;

      case 'canceled':
        await cancelOrder(payment.metadata.orderId);
        await releaseInventory(payment.metadata.orderId);
        break;

      case 'expired':
        await expireOrder(payment.metadata.orderId);
        break;

      case 'refunded':
        await processRefund(payment.metadata.orderId, payment.amountRefunded);
        break;

      default:
        console.log(`Unhandled payment status: ${payment.status}`);
    }
  } catch (error) {
    console.error(`Error processing webhook for ${paymentId}:`, error);
  }
});

Full Payment Object from the API

When you call mollieClient.payments.get(paymentId), you receive a comprehensive payment object. Here's a realistic example:

{
  "resource": "payment",
  "id": "tr_7UhSN1zuXS",
  "mode": "live",
  "createdAt": "2026-04-11T10:23:41+00:00",
  "amount": {
    "value": "49.99",
    "currency": "EUR"
  },
  "description": "Annual Pro Plan",
  "method": "ideal",
  "metadata": {
    "orderId": "ORD-2026-1847",
    "plan": "pro-annual"
  },
  "status": "paid",
  "paidAt": "2026-04-11T10:24:15+00:00",
  "amountRefunded": {
    "value": "0.00",
    "currency": "EUR"
  },
  "amountRemaining": {
    "value": "49.99",
    "currency": "EUR"
  },
  "locale": "nl_NL",
  "countryCode": "NL",
  "profileId": "pfl_QkEhN94Ba",
  "settlementAmount": {
    "value": "49.70",
    "currency": "EUR"
  },
  "details": {
    "consumerName": "Jan de Vries",
    "consumerAccount": "NL53INGB0654422370",
    "consumerBic": "INGBNL2A"
  },
  "redirectUrl": "https://yourapp.com/order/success",
  "webhookUrl": "https://yourapp.com/api/webhooks/mollie",
  "_links": {
    "self": {
      "href": "https://api.mollie.com/v2/payments/tr_7UhSN1zuXS",
      "type": "application/hal+json"
    },
    "checkout": {
      "href": "https://www.mollie.com/checkout/ideal/7UhSN1zuXS",
      "type": "text/html"
    }
  }
}

Key fields to note:

  • status is the field you'll use most — it drives your business logic
  • metadata contains the custom data you passed when creating the payment (use it to link back to your order)
  • details contains payment-method-specific information (bank name, card details, etc.)
  • settlementAmount tells you the actual amount after Mollie's fees

How to Test Mollie Webhooks with HookRay

Testing Mollie webhooks during development is challenging because Mollie requires a publicly accessible HTTPS URL. Your local localhost:3000 won't work. Even Mollie's test mode requires a reachable endpoint. This is where HookRay makes the workflow seamless.

Step 1: Get a HookRay Endpoint

  1. Go to hookray.com/app
  2. A unique webhook URL is generated instantly — no signup required
  3. Copy the URL (it looks like https://hookray.com/api/wh/your-unique-id)

Step 2: Use the HookRay URL with Mollie

Pass your HookRay URL as the webhookUrl when creating test payments:

const payment = await mollieClient.payments.create({
  amount: { currency: 'EUR', value: '10.00' },
  description: 'Test Payment',
  redirectUrl: 'https://yourapp.com/order/success',
  webhookUrl: 'https://hookray.com/api/wh/your-unique-id',
  metadata: { orderId: 'TEST-001' }
});

Or paste the HookRay URL into the Mollie Dashboard webhook settings for account-wide testing.

Step 3: Complete a Test Payment

In Mollie's test mode, use the test payment flow:

  1. Create a payment using the Mollie API with your test API key
  2. Redirect to the checkout URL returned by Mollie
  3. Select a payment status (paid, failed, canceled, expired) in the test checkout screen
  4. Mollie sends a webhook to your HookRay URL

Step 4: Inspect the Webhook in Real Time

Switch to your HookRay dashboard. You'll see the incoming Mollie webhook request instantly:

  • View the form-encoded body (id=tr_xxxxx)
  • Check HTTP headers sent by Mollie (Content-Type, User-Agent)
  • Compare multiple webhook deliveries side by side
  • Replay the webhook to your local server once your handler is ready

This lets you see exactly what Mollie sends before writing a single line of handler code. You can test every payment status transition — paid, failed, canceled, expired — and verify your endpoint receives them all correctly.

Step 5: Build with Confidence

With HookRay capturing real Mollie webhook data, your development loop becomes:

  1. Create a test payment via the Mollie API
  2. Complete it with the desired status in the Mollie test checkout
  3. See the webhook arrive in HookRay instantly
  4. Build your handler based on the actual request format
  5. Replay captured webhooks to your local endpoint for testing

For more Mollie-specific webhook patterns, visit our Mollie webhook reference.

Verifying Mollie Webhook Authenticity

Unlike providers that sign webhook payloads with HMAC, Mollie's webhook verification model is simpler because of the callback-based design. Since the webhook body only contains an ID, and your server fetches the actual data from the Mollie API using your secret API key, authenticity is inherently verified — only someone with your API key could get the payment data.

However, you should still protect your webhook endpoint from abuse:

1. Validate the Payment ID Format

Mollie payment IDs follow a consistent format (tr_ prefix followed by alphanumeric characters). Reject anything that doesn't match:

app.post('/api/webhooks/mollie', async (req, res) => {
  const paymentId = req.body.id;

  // Validate format before making any API calls
  if (!paymentId || !/^tr_[a-zA-Z0-9]+$/.test(paymentId)) {
    return res.status(200).send('OK'); // Still return 200 to avoid retries
  }

  // Proceed with API call...
});

2. Verify via API Response

If someone sends a fake payment ID, the Mollie API will return a 404. Your handler should catch this gracefully:

try {
  const payment = await mollieClient.payments.get(paymentId);
  // Valid payment — process it
} catch (error) {
  if (error.status === 404) {
    console.warn(`Unknown payment ID received: ${paymentId}`);
    return; // Ignore — likely a spoofed request
  }
  throw error; // Re-throw real errors
}

3. Verify Metadata Matches Your Records

Always cross-reference the payment metadata with your own database:

const payment = await mollieClient.payments.get(paymentId);
const order = await db.orders.findByOrderId(payment.metadata.orderId);

if (!order) {
  console.warn(`Payment ${paymentId} has unknown orderId: ${payment.metadata.orderId}`);
  return;
}

4. Use a Secret Path or Token

Add a hard-to-guess segment to your webhook URL as an extra layer:

https://yourapp.com/api/webhooks/mollie?token=a1b2c3d4e5f6

Validate the token in your handler before processing.

Common Mollie Webhook Issues and Debugging

Even with a straightforward setup, Mollie webhooks can behave unexpectedly. Here are the issues developers encounter most often.

Webhook Not Being Called

Symptoms: You create a payment, complete it in the test checkout, but your endpoint never receives a request.

Causes and fixes:

  1. URL not publicly accessible — Mollie cannot reach localhost, 127.0.0.1, or internal network addresses. Use HookRay for instant public URLs during development.

  2. Missing HTTPS in live mode — Live mode requires HTTPS. Test mode allows HTTP but HTTPS is still recommended.

  3. Webhook URL not set — Verify the webhookUrl parameter is included in your payment creation call. Check for typos in the URL.

  4. Firewall or WAF blocking — Your server's firewall or web application firewall might block Mollie's requests. Allowlist the Mollie User-Agent (Mollie HTTP) or Mollie's IP ranges.

  5. DNS not resolved — If you recently set up a domain, DNS propagation might not be complete. Verify your domain resolves correctly.

Receiving Webhooks But Failing to Process

Symptoms: Mollie sends the webhook, but your handler crashes or produces errors.

Causes and fixes:

  1. Parsing as JSON instead of form-encoded — Mollie sends application/x-www-form-urlencoded, not JSON. In Express, use express.urlencoded() middleware, not express.json().

  2. Wrong API key — If you created the payment with a test key but your webhook handler uses a live key (or vice versa), the API call to fetch payment details will fail with a 404.

  3. Async/await errors silently swallowed — If you respond with 200 before processing and don't catch errors in the async processing, failures go unnoticed. Always wrap async processing in try/catch with proper logging.

Duplicate Webhook Deliveries

Symptoms: Your handler processes the same payment multiple times.

Causes and fixes:

  1. Mollie retries on non-2xx — If your endpoint doesn't return a 2xx status code quickly enough (within ~15 seconds), Mollie retries. Always return 200 immediately, then process asynchronously.

  2. Multiple status changes — A single payment can trigger multiple webhooks as it transitions through statuses (open -> pending -> paid). This is normal. Make your handler idempotent.

// Idempotent handler example
const payment = await mollieClient.payments.get(paymentId);
const order = await db.orders.findByOrderId(payment.metadata.orderId);

// Only process if the order status hasn't already been updated
if (order.status !== payment.status) {
  await db.orders.updateStatus(order.id, payment.status);
  // Trigger side effects...
}

Testing Without a Public URL

Symptoms: You want to test webhooks locally without deploying.

Solution: Use HookRay to capture Mollie webhooks in real time, inspect the payloads, and replay them to your local development server. This is faster and more reliable than setting up tunneling tools.

Best Practices for Production Mollie Webhooks

  1. Always respond 200 first — Return 200 OK immediately, then process asynchronously. Mollie interprets slow responses as failures and retries.

  2. Build idempotent handlers — The same webhook may arrive multiple times. Use the payment ID and status as a compound key to prevent duplicate processing.

  3. Use metadata — Pass your internal order ID, customer ID, and plan details in the metadata field when creating payments. This links Mollie payments to your domain objects without extra database lookups.

  4. Log every webhook — Capture the payment ID, fetched status, and processing result for every webhook. This audit trail is invaluable for debugging payment issues.

  5. Handle all statuses — Don't just handle paid. Implement handlers for failed, canceled, expired, and refunded to keep your system consistent.

  6. Test every status transition — Use Mollie's test mode with HookRay to verify your handler works for every possible status. The test checkout lets you simulate paid, failed, canceled, and expired states.

  7. Monitor webhook processing — Track success rates, processing times, and error rates. Set alerts for when webhook processing starts failing.

Start Testing Your Mollie Webhooks Today

Setting up Mollie webhooks is straightforward, but building reliable payment integrations requires seeing real webhook requests, testing every status transition, and debugging delivery issues. HookRay gives you instant webhook URLs with real-time inspection, search, and replay — everything you need to build and test Mollie integrations with confidence.

Get started in 5 seconds:

  1. Go to hookray.com/app
  2. Copy your unique webhook URL
  3. Pass it as webhookUrl in your Mollie payment creation call
  4. See webhooks arrive in real time

No signup. No credit card. Just fast, reliable webhook testing.


Building a Mollie payment integration? Try HookRay free — inspect, debug, and replay your Mollie webhooks in real time.

Ready to test your webhooks?

Get a free webhook URL in 5 seconds. No signup required.

Start Testing — Free