Kommo + Lemon Squeezy: автоматический учёт оплат SaaS-подписок в CRM

Lemon Squeezy - Merchant of Record платформа для SaaS-продуктов: принимает платежи, обрабатывает НДС, возвраты, подписки. Kommo - CRM, где ведётся воронка продаж. Без интеграции момент оплаты существует в Lemon Squeezy, но не в Kommo: менеджер не видит, заплатил ли клиент, и сделка зависает в ручном режиме. Интеграция замыкает этот разрыв: событие order_created от Lemon Squeezy автоматически закрывает сделку в Kommo и создаёт запись об оплате.

Lemon Squeezy набирает популярность у indie SaaS и небольших B2B продуктов как альтернатива Paddle и Stripe. В отличие от Stripe, Lemon Squeezy берёт на себя налоговую ответственность (Merchant of Record) - компания не занимается VAT, sales tax и соблюдением регуляций самостоятельно. Это критично для команд, продающих в США, ЕС и других регионах одновременно.

Ключевая техническая возможность: Lemon Squeezy позволяет передавать произвольные данные через checkout_data.custom при создании checkout-сессии. Эти данные возвращаются в каждом webhook-событии в поле meta.custom_data. Именно через этот механизм передаётся kommo_lead_id - идентификатор сделки в Kommo.

Почему нативная интеграция не существует

На момент написания этой статьи Lemon Squeezy не имеет готового коннектора с Kommo. Zapier имеет триггеры для Lemon Squeezy, но не умеет работать с кастомными полями Kommo API и не поддерживает webhook-верификацию. Единственный надёжный способ - прямая интеграция через API.

Merchant of Record - модель, при которой платёжная платформа (а не ваша компания) является продавцом перед законом: несёт ответственность за налоги, возвраты, соблюдение PCI DSS. Для SaaS компаний это означает отсутствие необходимости регистрировать VAT в каждой стране продаж.

Техническая архитектура

Kommo CRM
  -> deal.status_changed (к стадии "Отправлен счёт")
  -> POST /v1/checkouts {custom: {kommo_lead_id}}
  <- checkout URL
  -> Записать URL как поле сделки / отправить клиенту

Клиент
  -> Открыть checkout URL, оплатить

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

Ваш сервер
  -> Верифицировать X-Signature (HMAC-SHA256)
  -> PUT /api/v4/leads/{kommo_lead_id} {status_id: 142}  # Успешно реализовано
  -> POST /api/v4/leads/{kommo_lead_id}/notes {text: "Оплата получена: $99"}

Реализация: создание checkout

При переходе сделки в нужную стадию формируем checkout-сессию в 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"]

Полученный URL сохраняется в кастомное поле сделки в Kommo и/или отправляется клиенту через email.

Реализация: обработка webhook

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,
        }],
    )

Обратный поток: self-serve signups

Если продукт работает по модели self-serve, клиент оплачивает напрямую через встроенный checkout на сайте (без участия менеджера). В этом случае интеграция работает в обратном направлении:

  1. Клиент оплачивает на сайте - в checkout_data.custom передаётся источник (utm_source, plan name)
  2. Webhook order_created -> создать новую сделку в Kommo через POST /api/v4/leads
  3. Создать контакт и привязать к сделке
  4. Назначить менеджера по round-robin или зоне ответственности

Это позволяет менеджерам видеть всех платящих пользователей в Kommo и инициировать upsell/cross-sell процессы.

Реальный кейс

B2B SaaS для управления командами: продажи ведутся через Kommo, оплата - через Lemon Squeezy. До интеграции: менеджеры проверяли LS вручную 2-3 раза в день и вручную обновляли статусы в CRM. Около 15% сделок зависали в промежуточном статусе дольше суток.

После интеграции:

  • Статус сделки обновляется в течение 30 секунд после оплаты
  • Команда CSM получает задачу при отмене подписки автоматически
  • Среднее время от оплаты до онбординга сократилось с 6 часов до 45 минут

Для кого актуально

Компании, которые:

  • Используют Kommo как CRM для B2B-продаж
  • Принимают платежи через Lemon Squeezy (или рассматривают переход с Stripe на MOR-модель)
  • Тратят время на ручную синхронизацию статусов между CRM и платёжной системой

Особенно актуально для продуктов с годовыми/квартальными подписками, где важно отслеживать продления и отмены в CRM-контексте.

Если вы уже работаете с кастомными интеграциями в Kommo CRM, добавление Lemon Squeezy займёт 1-2 рабочих дня на разработку и тестирование.

Часто задаваемые вопросы

Как Lemon Squeezy подтверждает webhook-запросы?

Каждый POST-запрос содержит заголовок X-Signature - HMAC-SHA256 подпись тела запроса с использованием вашего signing secret (задаётся в настройках webhook в LS Dashboard). Верифицируйте подпись до обработки любых данных. Signing secret отличается от API ключа.

Можно ли передать несколько параметров в custom_data?

Да. checkout_data.custom принимает произвольный JSON-объект. Можно передавать kommo_lead_id, utm_source, plan_name, manager_id и любые другие данные. Все они вернутся в meta.custom_data каждого webhook-события.

Что происходит при возврате платежа?

Lemon Squeezy отправляет событие order_refunded. В обработчике создайте задачу в Kommo для менеджера с информацией о возврате и переведите сделку в статус “требует внимания” или восстановите в воронку.

Поддерживает ли Lemon Squeezy тестовые webhooks?

Да. В LS Dashboard есть режим Test Mode - создайте тестовые заказы без реальных платежей. Webhooks отправляются с теми же заголовками, что и боевые. Signing secret в Test Mode такой же, как в Production.

Итог

Kommo + Lemon Squeezy интеграция решает задачу синхронизации статусов между CRM и платёжной системой:

  • Создание checkout со встроенным kommo_lead_id в custom_data
  • Webhook-обработчик с HMAC-верификацией X-Signature
  • order_created + status: paid -> закрыть сделку, добавить заметку
  • subscription_cancelled -> создать задачу CSM
  • Self-serve: order_created -> создать новую сделку в Kommo

Если ваша команда тратит время на ручное отслеживание оплат между Lemon Squeezy и Kommo - опишите задачу команде Exceltic.dev. Реализуем интеграцию под ваш стек.

Ещё статьи

Все →