Webhooks: Best practices

🚧

This feature is in beta.

Creating Webhooks

When subscribing to events, we recommend consolidating them.
Ideally, you should have only one subscription that encompasses all the relevant event types (e.g., using a single webhook with multiple event types assigned to it).
This streamlined approach simplifies your architecture, minimizes the need for numerous subscriptions and helps prevent system overload due to excessive delivery URLs.

How to verify HMAC signature

To verify HMAC signature for a webhook, follow these steps:

  1. Retrieve the signature from the request header (X-Mollie-Signature) and obtain the shared secret key.
  2. Use this key to generate HMAC hash of the raw request body using the same algorithm (e.g., HMAC-SHA256).
  3. Finally, compare the generated hash with the received signature using a timing-safe comparison method to confirm the authenticity and integrity of the request.
<?php
// Shared secret generated by Mollie Webhooks system
$sharedSecret = 'your_shared_secret';

// Retrieve the signature from the headers (X-Mollie-Signature)
$headers = getallheaders();
$providedSignature = isset($headers['X-Mollie-Signature']) ? $headers['X-Mollie-Signature'] : null;

if (!$providedSignature) {
    http_response_code(400); // Bad Request
    echo "Missing signature.";
    exit;
}

// Retrieve the raw POST data
$payload = file_get_contents('php://input');

if (!$payload) {
    http_response_code(400); // Bad Request
    echo "Missing payload.";
    exit;
}

// Calculate the HMAC hash of the payload
$calculatedSignature = hash_hmac('sha256', $payload, $sharedSecret);

// Compare the calculated signature with the provided signature
if (!hash_equals($calculatedSignature, $providedSignature)) {
    http_response_code(403); // Forbidden
    echo "Invalid signature.";
    exit;
}

// Process the webhook payload
$data = json_decode($payload, true);

// Example: Log the payload
file_put_contents('webhook.log', print_r($data, true), FILE_APPEND);

http_response_code(200); // OK
echo "Webhook received and verified.";
?>
from flask import Flask, request, abort
import hmac
import hashlib

app = Flask(__name__)

# Shared secret provided by Mollie Webhooks system
SHARED_SECRET = 'your_shared_secret'

def verify_signature(payload, provided_signature):
    """
    Verify the webhook's authenticity by comparing the provided signature
    with the calculated HMAC signature.
    """
    # Calculate the HMAC signature
    calculated_signature = hmac.new(
        SHARED_SECRET.encode('utf-8'),
        payload,
        hashlib.sha256
    ).hexdigest()

    # Compare signatures securely
    return hmac.compare_digest(calculated_signature, provided_signature)

@app.route('/webhook', methods=['POST'])
def webhook():
    # Retrieve the signature from the headers
    provided_signature = request.headers.get('X-Mollie-Signature')
    if not provided_signature:
        abort(400, 'Missing signature.')

    # Get the raw payload
    payload = request.data
    if not payload:
        abort(400, 'Missing payload.')

    # Verify the signature
    if not verify_signature(payload, provided_signature):
        abort(403, 'Invalid signature.')

    # Process the webhook payload
    data = request.json  # Parse the JSON payload if needed

    # Example: Log the payload
    with open('webhook.log', 'a') as f:
        f.write(str(data) + '\n')

    return 'Webhook received and verified.', 200

if __name__ == '__main__':
    app.run(port=5000, debug=True)
const express = require('express');
const bodyParser = require('body-parser');
const crypto = require('crypto');

const app = express();
const PORT = 5000;

// Shared secret provided by Mollie Webhooks system
const SHARED_SECRET = 'your_shared_secret';

// Middleware to parse raw body for signature verification
app.use(bodyParser.raw({ type: '*/*' }));

/**
 * Verify the webhook's signature.
 * @param {Buffer} payload - Raw request body
 * @param {string} providedSignature - Signature from the request headers
 * @returns {boolean} - Whether the signature is valid
 */
function verifySignature(payload, providedSignature) {
    const calculatedSignature = crypto
        .createHmac('sha256', SHARED_SECRET)
        .update(payload)
        .digest('hex');

    // Use constant-time comparison to prevent timing attacks
    return crypto.timingSafeEqual(
        Buffer.from(calculatedSignature, 'utf-8'),
        Buffer.from(providedSignature, 'utf-8')
    );
}

app.post('/webhook', (req, res) => {
    // Retrieve the signature from the headers
    const providedSignature = req.headers['X-Mollie-Signature'];
    if (!providedSignature) {
        return res.status(400).send('Missing signature.');
    }

    // Get the raw payload
    const payload = req.body;
    if (!payload) {
        return res.status(400).send('Missing payload.');
    }

    // Verify the signature
    if (!verifySignature(payload, providedSignature)) {
        return res.status(403).send('Invalid signature.');
    }

    // Parse the payload as JSON (if applicable)
    let data;
    try {
        data = JSON.parse(payload.toString('utf-8'));
    } catch (err) {
        return res.status(400).send('Invalid JSON payload.');
    }

    // Example: Log the payload
    console.log('Verified webhook payload:', data);

    return res.status(200).send('Webhook received and verified.');
});

app.listen(PORT, () => {
    console.log(`Server running on http://localhost:${PORT}`);
});

How to rotate signing secrets

You can update the secret key used to verify the authenticity of incoming events in the Webhooks section of the Mollie Dashboard.
For any webhook endpoint, select the Roll secret option. This action will generate a new secret key and will automatically expire the old key within a day.
To enhance security, it’s advisable to regularly rotate your secrets, or do so promptly if you suspect the secret key has been compromised.

How to properly set up TLS

Transport Layer Security facilitates the safety of the data exchanged between a user’s browser and a website and protects it from interception or tampering.

To set up TLS properly:

  1. Obtain an SSL/TLS certificate from a trusted Certificate Authority and install it on your server.
  2. Configure your server (e.g., Nginx or Apache) to use the certificate, enforcing HTTPS and supporting only secure TLS versions (i.e. TLS 1.2 or 1.3).
  3. Disable weak ciphers, redirect HTTP traffic to HTTPS, and enable HTTP Strict Transport Security (HSTS) to prevent downgrade attacks.
  4. To ensure you stay up to date with the latest best practices, regularly update certificates and test your setup with tools like SSL Labs.

How to set up old and new webhooks to work in parallel

While we suggest maintaining separate endpoints for each system, ensuring they process incoming requests independently, it's also possible to reuse and share the same endpoint to accept requests from both systems.

To run payments webhooks and new webhook systems in parallel, you need to configure your application to support both systems simultaneously: the new Webhooks system sends a header X-Mollie-Signature within all requests containing the signed event payload and we recommend using this header to identify that it's a request using the new Webhooks, use it to verify authenticity and proceed with any different flows and actions required on your system.

Test the new system thoroughly (we suggest using the test mode), while monitoring both for discrepancies.

How to verify an event’s payload (using Webhook Events API)

Signature verification does not come without risks, since an attacker who gets access to the secret may be able to generate the right signature for a fraudulent message.

To eliminate this risk, you may want to verify an event's payload and its authenticity using the Webhook Events API by sending a GET request with the event’s unique identifier (eventID) to the API's endpoint.
The API will respond with the event's payload as a result, confirming whether the event is authentic and valid.