Kommo + Mollie: payment links and subscriptions from the sales pipeline

Kommo + Mollie: payment links and subscriptions from the sales pipeline

Mollie is a European payment gateway supporting iDEAL, SEPA Direct Debit, Bancontact, Klarna, and all major card networks. Unlike Stripe, Mollie is designed from the ground up for the EU market and does not require separate setup for each European payment method. Without a Kommo integration, a manager manually opens Mollie after Won, creates a payment, and sends the link to the client. With the integration, Won automatically generates a payment link with the correct amount and payment methods, and the payment.paid event instantly moves the deal to the next stage.

Why Mollie for EU teams

Mollie supports 25+ payment methods — most without additional applications:
iDEAL (Netherlands, 60%+ of online payments in the country)
SEPA Direct Debit (for B2B subscriptions in the Eurozone)
Bancontact (Belgium), Sofort (Germany/Austria), Przelewy24 (Poland)
Klarna (instalment payments), Apple Pay, Google Pay

For a team with clients across different EU countries, Mollie means one API instead of separate integrations with local gateways. Compared to payment links via Stripe, Mollie wins on European method coverage without additional approval forms.

What is synchronized

Kommo -> Mollie:
— Won -> create a Mollie Payment Link with the amount from the deal
— Won -> create a Mollie Customer + Subscription (for recurring)
— Deal amount change -> update the draft payment

Mollie -> Kommo:
payment.paid -> Note: “Mollie: payment received” + stage change
payment.failed -> Note + task for manager: “Payment failed”
payment.expired -> Note: “Link expired without payment”
subscription.charged_back -> Note + task: “Chargeback — verify”

Architecture

Kommo Webhook: deal moved to Won
  ↓ Backend
  1. GET /api/v4/leads/{id} + contacts
     -> email, name, amount, description
  2. Mollie API: POST /v2/payments
     -> amount, description, redirectUrl, webhookUrl
     -> method: ["ideal","creditcard","sepadirectdebit"] or null (client's choice)
  3. Kommo: PATCH /leads/{id}
     -> custom field mollie_payment_id = payment.id
     -> custom field mollie_checkout_url = payment._links.checkout.href
  4. Kommo: POST /leads/{id}/notes
     -> "Mollie: payment link sent"
  5. (optional) Send checkout URL to client via WhatsApp/email

Mollie Webhook: payment.paid
  ↓ Backend
  1. GET /v2/payments/{id} -> verify status
  2. Find deal by mollie_payment_id
  3. Kommo: PATCH /leads/{id} -> stage change "Payment received"
  4. Kommo: POST /leads/{id}/notes -> "Mollie: payment confirmed, method: {method}"

Mollie API: key requests

Base URL: https://api.mollie.com/v2.
Authentication: Bearer token — Authorization: Bearer {api_key}.
Test keys start with test_, production keys with live_.

Create a payment:

import requests

MOLLIE_API_KEY = "live_your_api_key"
MOLLIE_BASE_URL = "https://api.mollie.com/v2"

headers = {
    "Authorization": f"Bearer {MOLLIE_API_KEY}",
    "Content-Type": "application/json"
}

def create_payment(amount_eur: float, description: str,
                   redirect_url: str, webhook_url: str,
                   customer_email: str = None) -> dict:
    payload = {
        "amount": {
            "currency": "EUR",
            "value": f"{amount_eur:.2f}"  # Mollie requires a string "99.00"
        },
        "description": description,
        "redirectUrl": redirect_url,
        "webhookUrl": webhook_url,
        "locale": "nl_NL",   # or "en_US", "de_DE", etc.
    }
    if customer_email:
        payload["metadata"] = {"customer_email": customer_email}

    resp = requests.post(f"{MOLLIE_BASE_URL}/payments", headers=headers, json=payload)
    resp.raise_for_status()
    return resp.json()

def on_deal_won(lead: dict, contact: dict):
    amount = lead.get("price", 0) / 100  # Kommo stores in cents
    email = get_contact_email(contact)
    deal_id = lead["id"]

    payment = create_payment(
        amount_eur=float(amount),
        description=f"Payment for deal #{deal_id} - {lead.get('name', '')}",
        redirect_url=f"https://yoursite.com/payment/success?deal={deal_id}",
        webhook_url=f"https://your-backend.com/webhooks/mollie",
        customer_email=email
    )

    checkout_url = payment["_links"]["checkout"]["href"]
    payment_id = payment["id"]

    update_kommo_deal(deal_id, {
        "mollie_payment_id": payment_id,
        "mollie_checkout_url": checkout_url
    })
    create_kommo_note(deal_id, f"Mollie: payment link created\n{checkout_url}")

Handle Mollie Webhook:

Mollie does not sign webhooks with HMAC — instead, always make a GET /v2/payments/{id} to verify the status. Do not trust the status from the webhook body.

from flask import Flask, request

app = Flask(__name__)

@app.route("/webhooks/mollie", methods=["POST"])
def mollie_webhook():
    payment_id = request.form.get("id")  # Mollie sends form-encoded, not JSON
    if not payment_id:
        return "", 200

    # Verification: always request status via API
    resp = requests.get(
        f"{MOLLIE_BASE_URL}/payments/{payment_id}",
        headers=headers
    )
    if not resp.ok:
        return "", 200

    payment = resp.json()
    status = payment.get("status")
    method = payment.get("method", "unknown")

    deal_id = find_deal_by_field("mollie_payment_id", payment_id)
    if not deal_id:
        return "", 200

    if status == "paid":
        update_kommo_deal(deal_id, {"stage_id": STAGE_PAYMENT_RECEIVED})
        create_kommo_note(deal_id,
            f"Mollie: payment received (method: {method}, status: paid)")

    elif status == "failed":
        create_kommo_note(deal_id, "Mollie: payment failed")
        create_kommo_task(deal_id, "Clarify payment method - Mollie recorded an error")

    elif status == "expired":
        create_kommo_note(deal_id, "Mollie: payment link expired without payment")
        create_kommo_task(deal_id, "Send a new link - the previous one expired")

    return "", 200

Subscriptions (for recurring payments):

def create_mollie_subscription(customer_id: str, amount_eur: float,
                                interval: str = "1 month") -> dict:
    resp = requests.post(
        f"{MOLLIE_BASE_URL}/customers/{customer_id}/subscriptions",
        headers=headers,
        json={
            "amount": {"currency": "EUR", "value": f"{amount_eur:.2f}"},
            "interval": interval,  # "1 month", "1 week", "1 year"
            "description": "Monthly subscription",
            "webhookUrl": "https://your-backend.com/webhooks/mollie",
        }
    )
    resp.raise_for_status()
    return resp.json()

Important: Mollie webhooks send data as application/x-www-form-urlencoded, not JSON. Use request.form.get("id"), not request.json.

European payment methods: what to specify in method

If "method": null is passed — Mollie shows the client all available methods for their country. For a specific market:

# Netherlands - iDEAL
payload["method"] = "ideal"

# Germany - priority SEPA + cards
payload["method"] = ["sepadirectdebit", "creditcard"]

# Belgium
payload["method"] = ["bancontact", "creditcard"]

# International B2B
payload["method"] = ["creditcard", "sepadirectdebit", "paypal"]

Real-world case

SaaS (Netherlands, B2B, Kommo + Mollie, 30–40 new clients per month):

  • Before: a manager manually created a payment in Mollie after Won and sent the link by email. Delay of 1–4 hours. Clients from the Netherlands wanted iDEAL, from Germany — SEPA, from Belgium — Bancontact. Three different scenarios — three manual steps.
  • After: Won -> multi-method link within 5 seconds. The client sees their preferred method automatically by geolocation. payment.paid -> Note + stage change without manager involvement.
  • Additionally: link expiry after 48 hours without payment -> automatic task for the manager -> resend. Conversion of unpaid payments improved by 22%.

Who this is relevant for

  • EU companies with clients in the Netherlands, Germany, Belgium, Poland — Mollie covers local methods better than Stripe there
  • SaaS with recurring subscriptions via SEPA Direct Debit
  • B2B with large invoices — Mollie supports payments up to €100k without separate approval
  • Teams without a Merchant of Record — unlike Paddle, Mollie does not take on tax liability

Frequently asked questions

Mollie webhook — is signature verification needed?

No — Mollie does not sign webhooks with HMAC. Instead, always make a GET /v2/payments/{id} to verify the actual status. Do not process a payment based solely on the webhook body — this is a security vulnerability.

Does Mollie support test payments without real money?

Yes. Test API keys (test_...) allow creating payments with simulation of all statuses: paid, failed, expired, cancelled. In the Mollie Dashboard -> Test mode — a full development environment.

How does Mollie differ from Stripe for EU?

Mollie is registered and regulated in the Netherlands (DNB), requires no separate approval for iDEAL/Bancontact/SEPA. Stripe supports the same methods, but some (SEPA Direct Debit) require an application. For the NL/BE market, Mollie means less paperwork. For a global market and Stripe Connect — Stripe.

How do I pass customer data to Mollie for SEPA?

For SEPA Direct Debit, first create a Mollie Customer (POST /customers with name and email), then use the customerId when creating a payment. The client completes the mandating flow once; subsequent charges happen without their involvement.

Summary

  • Mollie API: Bearer token, https://api.mollie.com/v2
  • Create payment: POST /paymentsamount.value must be a string "99.00"
  • Webhook: arrives as application/x-www-form-urlencoded with id — always verify via GET /payments/{id}
  • No HMAC signature — verification via API request, not via header
  • Store mollie_payment_id in a Kommo custom field for reverse lookup
  • Method null = client chooses from available options in their country

If you work with EU clients and want to automate invoicing via Mollie from the Kommo pipeline — describe the payment methods in use. Exceltic.dev will configure the mapping and webhook handler.

More articles

All →