Kommo + GoCardless: automatic direct debit from the sales pipeline

Kommo + GoCardless: automatic direct debit from the sales pipeline

GoCardless is a European platform for direct debit payments: SEPA Direct Debit (EU), BACS (UK), ACH (US). Unlike Stripe or Mollie, GoCardless specializes in recurring payments via bank accounts — no card transaction fees and with a higher success rate for subscription businesses. Without Kommo integration, the manager creates a mandate manually after Won. With integration, Won automatically initiates the mandate collection process — the client receives a link to authorize debits within minutes of the deal being closed.

GoCardless vs Stripe for SaaS subscriptions in EU

GoCardless (SEPA DD):
— Fixed fee: €0.20–0.60 per transaction (not a % of the amount)
— No percentage cap — significantly cheaper than cards on large subscriptions
— Mandate given once -> recurring debits without client involvement
— Success rate 97–99% (cards: 85–92% due to expirations, blocks)
— Ideal for B2B SaaS with monthly/quarterly invoices

Stripe (card payments):
— % of transaction (2.9% + $0.30 or local EU rates)
— Cards expire, get blocked -> involuntary churn
— Instant payment (vs 2–5 business days for DD)

For custom Kommo integrations with European SaaS, GoCardless is often preferred specifically because of the fee structure and reliability.

What gets synchronized

Kommo -> GoCardless:
— Won -> create Customer in GoCardless with deal data
— Won -> create Redirect Flow (link for client to authorize the mandate)
— Won -> send mandate link to client (via Note or email)
— After mandate received -> create Subscription (plan, amount, interval)

GoCardless -> Kommo:
payment.paid -> Note: “GoCardless: payment received, €{amount}”
payment.failed -> Note + task for manager: “Payment failed — contact client”
mandate.cancelled -> Note + task: “Mandate cancelled — reissue”
subscription.cancelled -> Note: “Subscription cancelled”

GoCardless API: key requests

Base URL: https://api.gocardless.com.
Authentication: Authorization: Bearer {access_token}.
Access Token: GoCardless Dashboard -> Developers -> Create Access Token.
API version header: GoCardless-Version: 2015-07-06 (required).

import requests

GC_TOKEN = "your_access_token"
GC_BASE_URL = "https://api.gocardless.com"
GC_HEADERS = {
    "Authorization": f"Bearer {GC_TOKEN}",
    "GoCardless-Version": "2015-07-06",
    "Content-Type": "application/json",
}

def create_customer(name: str, email: str, company_name: str = "") -> dict:
    payload = {
        "customers": {
            "email": email,
            "given_name": name.split()[0] if name else "",
            "family_name": " ".join(name.split()[1:]) if len(name.split()) > 1 else "",
            "company_name": company_name,
        }
    }
    resp = requests.post(f"{GC_BASE_URL}/customers", headers=GC_HEADERS, json=payload)
    resp.raise_for_status()
    return resp.json()["customers"]

def create_redirect_flow(customer_id: str, description: str,
                         success_redirect_url: str, session_token: str) -> dict:
    # Creates a link for the client to authorize the mandate
    payload = {
        "redirect_flows": {
            "description": description,
            "session_token": session_token,  # unique string for the session
            "success_redirect_url": success_redirect_url,
            "prefilled_customer": {"id": customer_id},
        }
    }
    resp = requests.post(f"{GC_BASE_URL}/redirect_flows", headers=GC_HEADERS, json=payload)
    resp.raise_for_status()
    return resp.json()["redirect_flows"]

def complete_redirect_flow(redirect_flow_id: str, session_token: str) -> dict:
    # Complete redirect flow after client returns, get mandate_id
    payload = {"data": {"session_token": session_token}}
    resp = requests.post(
        f"{GC_BASE_URL}/redirect_flows/{redirect_flow_id}/actions/complete",
        headers=GC_HEADERS,
        json=payload,
    )
    resp.raise_for_status()
    return resp.json()["redirect_flows"]

def create_subscription(mandate_id: str, amount_cents: int,
                        currency: str, interval_unit: str,
                        name: str) -> dict:
    # interval_unit: "monthly" | "weekly" | "yearly"
    # amount_cents: amount in cents/euro cents (49.00 EUR = 4900)
    payload = {
        "subscriptions": {
            "amount": amount_cents,
            "currency": currency,
            "interval_unit": interval_unit,
            "name": name,
            "links": {"mandate": mandate_id},
        }
    }
    resp = requests.post(f"{GC_BASE_URL}/subscriptions", headers=GC_HEADERS, json=payload)
    resp.raise_for_status()
    return resp.json()["subscriptions"]

def on_deal_won(lead: dict, contact: dict):
    import uuid
    email = get_contact_email(contact)
    name = contact["name"]
    company = get_custom_field(lead, COMPANY_FIELD_ID) or ""
    amount = lead.get("price", 0)
    plan = get_custom_field(lead, PLAN_FIELD_ID) or "Growth"

    # Create Customer
    customer = create_customer(name, email, company)
    customer_id = customer["id"]

    # Create Redirect Flow - link for the client
    session_token = str(uuid.uuid4())
    flow = create_redirect_flow(
        customer_id=customer_id,
        description=f"Authorize debits - {plan} plan",
        success_redirect_url="https://yourapp.com/payment/confirmed",
        session_token=session_token,
    )

    # Save data in Kommo
    update_kommo_deal(lead["id"], {
        "gc_customer_id": customer_id,
        "gc_redirect_flow_id": flow["id"],
        "gc_session_token": session_token,
    })

    create_kommo_note(lead["id"],
        f"GoCardless: mandate awaiting authorization\n"
        f"Client link: {flow['redirect_url']}")

Webhook after client authorization (redirect_flow.completed):

@app.route("/webhooks/gocardless/redirect", methods=["GET"])
def gc_redirect_complete():
    # Client returned after authorizing the mandate
    redirect_flow_id = request.args.get("redirect_flow_id")
    deal_id = find_deal_by_field("gc_redirect_flow_id", redirect_flow_id)
    if not deal_id:
        return "OK", 200

    session_token = get_deal_field(deal_id, "gc_session_token")
    flow = complete_redirect_flow(redirect_flow_id, session_token)
    mandate_id = flow["links"]["mandate"]

    amount = get_deal_amount(deal_id)
    sub = create_subscription(
        mandate_id=mandate_id,
        amount_cents=int(amount * 100),
        currency="EUR",
        interval_unit="monthly",
        name=f"Subscription #{deal_id}",
    )

    update_kommo_deal(deal_id, {"gc_mandate_id": mandate_id,
                                "gc_subscription_id": sub["id"]})
    create_kommo_note(deal_id,
        f"GoCardless: mandate received, subscription created (ID: {sub['id']})")
    return "OK", 200

Handling GoCardless Webhook (payment events):

import hmac, hashlib

GC_WEBHOOK_SECRET = "your_webhook_secret"

@app.route("/webhooks/gocardless", methods=["POST"])
def gc_webhook():
    # Signature verification
    sig = request.headers.get("Webhook-Signature", "")
    expected = hmac.new(
        GC_WEBHOOK_SECRET.encode(), request.data, hashlib.sha256
    ).hexdigest()
    if not hmac.compare_digest(expected, sig):
        return "", 498

    payload = request.json
    for event in payload.get("events", []):
        resource_type = event.get("resource_type")  # "payments", "mandates", "subscriptions"
        action = event.get("action")               # "paid", "failed", "cancelled" etc.
        links = event.get("links", {})

        if resource_type == "payments":
            payment_id = links.get("payment")
            mandate_id = links.get("mandate")
            deal_id = find_deal_by_field("gc_mandate_id", mandate_id)
            if not deal_id:
                continue
            amount = event.get("details", {}).get("amount", 0) / 100

            if action == "paid":
                create_kommo_note(deal_id,
                    f"GoCardless: payment received - €{amount:.2f}")

            elif action == "failed":
                reason = event.get("details", {}).get("description", "unknown")
                create_kommo_note(deal_id,
                    f"GoCardless: payment failed - {reason}")
                create_kommo_task(deal_id,
                    "GoCardless: contact client - payment issue")

        elif resource_type == "mandates" and action == "cancelled":
            mandate_id = links.get("mandate")
            deal_id = find_deal_by_field("gc_mandate_id", mandate_id)
            if deal_id:
                create_kommo_note(deal_id, "GoCardless: mandate cancelled by client")
                create_kommo_task(deal_id,
                    "GoCardless: reissue mandate or issue invoice alternatively")

    return "", 204

Sandbox for testing

GoCardless provides a sandbox environment (https://api-sandbox.gocardless.com) — a full copy of the production API. Test bank account details are generated in the Dashboard. Webhooks can be tested via ngrok or GoCardless -> Developers -> Webhook endpoints -> Send test event.

Real-world case

EU SaaS company (Netherlands, 40–60 new clients per month, Kommo + GoCardless):

  • Before: after Won, the manager manually sent the GoCardless link via email. Delay of 1–2 hours. 30% of clients did not complete the mandate authorization — manual follow-up required.
  • After: Won -> GoCardless link in Note within 5 seconds + automatic email to client. Conversion to completed mandate grew to 89% (was 67%) — the link arrives while the client is still in “purchase mode.”
  • Additionally: payment.failed -> task for manager on the same day. Involuntary churn dropped by 31% — the team responds before the end of the billing period.

Who this is relevant for

  • EU SaaS and B2B companies with monthly/quarterly payments
  • Teams with high MRR per client ($200+) — GoCardless fixed fee is more economical than Stripe’s %
  • Companies in UK/Netherlands/Germany/France — SEPA and BACS are native
  • 20+ active subscriptions — below that volume, manual management is still manageable

Frequently asked questions

GoCardless vs Stripe for EU SaaS — when to choose which?

GoCardless: monthly B2B subscriptions in EU, high average deal value ($200–5,000/mo), no need for instant payments. Stripe: one-time payments, e-commerce, instant payment required, international market outside EU. Both can be used together: GoCardless for subscriptions, Stripe for upgrades and one-time payments.

How does SEPA Direct Debit work — how many days until the debit?

After mandate authorization, the first debit takes 2–3 business days (client notification + T+2/T+3). Recurring ones — standard T+2. GoCardless automatically notifies the client before each debit (mandatory under SEPA rules, typically 3+ days in advance).

For SEPA DD — no, the client must authorize the mandate. Alternative: SEPA Instant Credit Transfer (not GoCardless) or use GoCardless Instant Bank Pay (Faster Payments, UK only). For EU, the most reliable approach is a redirect flow with a branded GoCardless page.

Does GoCardless support multiple currencies?

Yes: EUR (SEPA), GBP (BACS), USD (ACH), SEK, DKK, AUD, NZD, CAD. Each currency requires a separate Scheme in the account. Russian ruble and most EM currencies are not supported.

Summary

  • GoCardless API: Authorization: Bearer {token} + GoCardless-Version: 2015-07-06
  • Flow: create customer -> create redirect flow -> client authorizes -> complete flow -> create subscription
  • Webhook: HMAC-SHA256 via Webhook-Signature, events in events[] array
  • Key events: payment.paid, payment.failed, mandate.cancelled
  • Sandbox: https://api-sandbox.gocardless.com — full production copy
  • Advantage vs cards: fixed fee + 97–99% success rate for subscriptions

If you use GoCardless and Kommo and want to automate mandate collection on Won — describe your plan structure and billing intervals. Exceltic.dev will configure the redirect flow and payment event handling.

More articles

All →