Kommo + Lemon Squeezy: Automatic SaaS Payment Tracking in CRM

Lemon Squeezy is a Merchant of Record platform for SaaS products: it handles payments, VAT, refunds, and subscriptions. Kommo is the CRM where your sales pipeline lives. Without an integration, the payment event exists in Lemon Squeezy but not in Kommo - managers can’t see whether a client has paid, and the deal stalls in manual limbo. The integration closes this gap: an order_created event from Lemon Squeezy automatically closes the deal in Kommo and creates a payment record.

Lemon Squeezy is gaining traction among indie SaaS teams and small B2B products as an alternative to Paddle and Stripe. Unlike Stripe, Lemon Squeezy takes on tax liability as a Merchant of Record - your company doesn’t have to handle VAT, sales tax, or regulatory compliance on its own. This is critical for teams selling simultaneously in the US, EU, and other regions.

The key technical capability: Lemon Squeezy lets you pass arbitrary data through checkout_data.custom when creating a checkout session. That data is returned in every webhook event under meta.custom_data. This is exactly the mechanism used to pass kommo_lead_id - the deal identifier in Kommo.

Why a native integration doesn’t exist

At the time of writing, Lemon Squeezy has no pre-built connector for Kommo. Zapier has Lemon Squeezy triggers, but it can’t work with Kommo’s custom API fields and doesn’t support webhook verification. The only reliable approach is a direct API integration.

Merchant of Record is a model where the payment platform (not your company) is the seller of record under the law: it bears responsibility for taxes, refunds, and PCI DSS compliance. For SaaS companies this means no need to register for VAT in every country you sell to.

Technical architecture

Kommo CRM
  -> deal.status_changed (to stage "Invoice Sent")
  -> POST /v1/checkouts {custom: {kommo_lead_id}}
  <- checkout URL
  -> Save URL as a deal field / send to client

Client
  -> Open checkout URL, complete payment

Lemon Squeezy
  -> POST /your-server/webhooks/lemon {event: order_created, meta.custom_data.kommo_lead_id}

Your server
  -> Verify X-Signature (HMAC-SHA256)
  -> PUT /api/v4/leads/{kommo_lead_id} {status_id: 142}  # Successfully closed
  -> POST /api/v4/leads/{kommo_lead_id}/notes {text: "Payment received: $99"}

Implementation: creating a checkout

When a deal moves to the target stage, create a checkout session in Lemon Squeezy:

import requests, os

LS_API_KEY   = os.environ["LEMON_SQUEEZY_API_KEY"]
LS_STORE_ID  = os.environ["LS_STORE_ID"]
LS_VARIANT_ID = os.environ["LS_VARIANT_ID"]   # ID тарифного плана

def create_checkout(kommo_lead_id: str, client_email: str, client_name: str) -> str:
    # Create Lemon Squeezy checkout and return URL.
    r = requests.post(
        "https://api.lemonsqueezy.com/v1/checkouts",
        headers={
            "Authorization": f"Bearer {LS_API_KEY}",
            "Accept":        "application/vnd.api+json",
            "Content-Type":  "application/vnd.api+json",
        },
        json={
            "data": {
                "type": "checkouts",
                "attributes": {
                    "checkout_data": {
                        "email": client_email,
                        "name":  client_name,
                        "custom": {
                            "kommo_lead_id": str(kommo_lead_id)
                        }
                    },
                    "expires_at": None,   # без срока истечения
                    "preview":    False,
                },
                "relationships": {
                    "store":   {"data": {"type": "stores",   "id": LS_STORE_ID}},
                    "variant": {"data": {"type": "variants", "id": LS_VARIANT_ID}},
                }
            }
        },
        timeout=10,
    )
    r.raise_for_status()
    return r.json()["data"]["attributes"]["url"]

The resulting URL is saved to a custom deal field in Kommo and/or sent to the client via email.

Implementation: webhook handler

import hmac, hashlib, os, time
from flask import Flask, request, jsonify

app = Flask(__name__)
KOMMO_DOMAIN = os.environ["KOMMO_DOMAIN"]
KOMMO_TOKEN  = os.environ["KOMMO_TOKEN"]
LS_SECRET    = os.environ["LS_WEBHOOK_SECRET"]

KOMMO_BASE   = f"https://{KOMMO_DOMAIN}/api/v4"
KOMMO_HEADERS = {"Authorization": f"Bearer {KOMMO_TOKEN}"}

KOMMO_WON_STATUS = 142   # ID статуса "Успешно реализовано"

def verify_signature(raw_body: bytes, header: str) -> bool:
    expected = hmac.new(LS_SECRET.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header)

@app.route("/webhooks/lemon", methods=["POST"])
def lemon_webhook():
    sig = request.headers.get("X-Signature", "")
    if not verify_signature(request.data, sig):
        return jsonify({"error": "invalid signature"}), 401

    data  = request.json
    event = data.get("meta", {}).get("event_name", "")
    custom = data.get("meta", {}).get("custom_data", {})
    lead_id = custom.get("kommo_lead_id")

    if not lead_id:
        return jsonify({"status": "no_lead_id"}), 200

    if event == "order_created":
        attrs = data["data"]["attributes"]
        status = attrs.get("status")

        if status == "paid":
            handle_payment(lead_id, attrs)

    elif event == "subscription_cancelled":
        handle_cancellation(lead_id, data["data"]["attributes"])

    return jsonify({"status": "ok"}), 200

def handle_payment(lead_id: str, attrs: dict):
    amount  = attrs.get("total", 0) / 100   # cents -> dollars
    product = attrs.get("first_order_item", {}).get("product_name", "")

    # Закрыть сделку как выигранную
    requests.patch(
        f"{KOMMO_BASE}/leads",
        headers=KOMMO_HEADERS,
        json=[{"id": int(lead_id), "status_id": KOMMO_WON_STATUS}],
    )

    # Добавить заметку об оплате
    requests.post(
        f"{KOMMO_BASE}/leads/{lead_id}/notes",
        headers=KOMMO_HEADERS,
        json=[{
            "note_type":  "common",
            "params":     {"text": f"Lemon Squeezy: оплата ${amount:.2f} - {product}"}
        }],
    )

def handle_cancellation(lead_id: str, attrs: dict):
    ends_at = attrs.get("ends_at", "")
    requests.post(
        f"{KOMMO_BASE}/leads/{lead_id}/tasks",
        headers=KOMMO_HEADERS,
        json=[{
            "task_type_id": 1,
            "text":         f"Клиент отменил подписку. Истекает: {ends_at}. Выяснить причину.",
            "complete_till": int(time.time()) + 86400,   # завтра
            "responsible_user_id": None,
        }],
    )

Reverse flow: self-serve signups

If your product runs on a self-serve model, the client pays directly through an embedded checkout on your website - without a sales rep involved. In this case the integration works in the opposite direction:

  1. Client pays on the website - checkout_data.custom carries the source (utm_source, plan name)
  2. Webhook order_created -> create a new deal in Kommo via POST /api/v4/leads
  3. Create a contact and link it to the deal
  4. Assign a manager via round-robin or ownership zone

This lets managers see every paying user in Kommo and initiate upsell/cross-sell workflows.

Real-world case

A B2B SaaS for team management: sales run through Kommo, payments through Lemon Squeezy. Before the integration, managers checked Lemon Squeezy manually 2-3 times a day and updated deal statuses in the CRM by hand. Around 15% of deals stayed stuck in an intermediate status for more than 24 hours.

After the integration:

  • Deal status updates within 30 seconds of payment
  • The CSM team automatically receives a task when a subscription is cancelled
  • Average time from payment to onboarding dropped from 6 hours to 45 minutes

Who this applies to

Companies that:

  • Use Kommo as their CRM for B2B sales
  • Accept payments through Lemon Squeezy (or are considering a move from Stripe to the MOR model)
  • Spend time manually syncing statuses between CRM and payment system

Especially relevant for products with annual or quarterly subscriptions, where tracking renewals and cancellations in a CRM context matters.

If you are already working with custom integrations in Kommo CRM, adding Lemon Squeezy takes 1-2 business days to build and test.

Frequently asked questions

How does Lemon Squeezy verify webhook requests?

Every POST request contains an X-Signature header - an HMAC-SHA256 signature of the request body using your signing secret (configured in the webhook settings inside the LS Dashboard). Always verify the signature before processing any data. The signing secret is different from your API key.

Can you pass multiple parameters in custom_data?

Yes. checkout_data.custom accepts an arbitrary JSON object. You can pass kommo_lead_id, utm_source, plan_name, manager_id, and any other data you need. All of it will be returned in meta.custom_data for every webhook event.

What happens on a refund?

Lemon Squeezy sends an order_refunded event. In your handler, create a task in Kommo for the manager with refund details and move the deal to a “needs attention” status or return it to the pipeline.

Does Lemon Squeezy support test webhooks?

Yes. The LS Dashboard has a Test Mode - create test orders without real payments. Webhooks are sent with the same headers as production. The signing secret in Test Mode is the same as in Production.

Summary

The Kommo + Lemon Squeezy integration solves the status synchronization problem between CRM and payment system:

  • Create a checkout with kommo_lead_id embedded in custom_data
  • Webhook handler with HMAC X-Signature verification
  • order_created + status: paid -> close the deal, add a note
  • subscription_cancelled -> create a CSM task
  • Self-serve: order_created -> create a new deal in Kommo

If your team is spending time manually tracking payments between Lemon Squeezy and Kommo - describe the task to the Exceltic.dev team. We will implement the integration for your stack.

More articles

All →