Kommo + Paddle: автоматическое закрытие сделки при оплате SaaS-подписки

Paddle - это Merchant of Record (MoR) для SaaS: он принимает оплату, берёт на себя НДС в каждой стране, выставляет инвойс от своего имени. Kommo - это CRM, в которой живёт воронка продаж. Без интеграции между ними данные разорваны: клиент оплатил подписку в Paddle, но сделка в Kommo по-прежнему висит в статусе «Переговоры». Менеджер не знает об оплате, автоматизация не запускается.

В проектах с SaaS-командами мы регулярно видим один сценарий: продажники ведут лиды в Kommo до этапа «Договор / оплата», а дальше — ручной процесс. Кто-то из отдела следит за Paddle Dashboard, кто-то пишет менеджеру в Slack «оплата прошла», тот закрывает сделку вручную. Это ломается при масштабировании и работает только пока команда маленькая.

В этой статье покажем, как связать Paddle с Kommo через webhook и custom_data - так, чтобы оплата автоматически закрывала сделку, прикрепляла инвойс и запускала онбординг.

Почему нативной интеграции нет

Paddle не имеет готового виджета для Kommo в App Marketplace. Это стандартная ситуация для платёжных MoR-платформ: их задача - обработать транзакцию, а не управлять CRM-воронкой. Zapier-интеграция существует, но она не решает главную проблему: Zapier не умеет надёжно привязать платёж к конкретной сделке в Kommo без дополнительной логики идентификации.

Merchant of Record - модель, при которой Paddle юридически выступает продавцом товара. Он собирает НДС по каждой стране, где находится покупатель, и перечисляет вам выручку за вычетом налогов и комиссии. Для EU и US SaaS это означает: вам не нужно регистрироваться плательщиком НДС в 40+ странах.

Ключевая особенность Paddle: custom_data

Paddle Billing v2 поддерживает custom_data - произвольный JSON-объект, который можно передать при создании транзакции или checkout-сессии. Этот объект автоматически копируется на подписку при её создании и появляется в теле каждого webhook-события.

Это решает главную проблему интеграции: вместо того чтобы искать сделку по email клиента постфактум, вы передаёте kommo_lead_id в момент оплаты - и webhook точно знает, какую сделку закрыть.

// При инициализации Paddle Checkout
Paddle.Checkout.open({
  items: [{ priceId: "pri_01abc...", quantity: 1 }],
  customer: { email: customerEmail },
  customData: {
    kommo_lead_id: "12345678",      // ID сделки в Kommo
    kommo_responsible_id: "42",     // ID менеджера в Kommo
    plan: "pro"
  }
});

Эти данные появятся в data.custom_data каждого webhook-события - transaction.completed, subscription.activated и далее по жизненному циклу подписки.

Архитектура интеграции

Поток данных:

  1. Менеджер ведёт сделку в Kommo до этапа «Оплата»
  2. Генерируется Paddle Checkout с kommo_lead_id в custom_data
  3. Клиент оплачивает - Paddle отправляет transaction.completed webhook
  4. Наш сервис обновляет сделку в Kommo: статус, поля, заметка
  5. При отмене подписки - Kommo получает уведомление

Реализация

Шаг 1 - верификация Paddle webhook:

from flask import Flask, request, abort
import hashlib, hmac, time

app = Flask(__name__)
PADDLE_WEBHOOK_SECRET = "pdl_ntfset_..."  # из Paddle Dashboard -> Notifications
KOMMO_DOMAIN          = "yourdomain.kommo.com"
KOMMO_TOKEN           = "your_kommo_long_lived_token"

def verify_paddle_signature(payload: bytes, signature_header: str) -> bool:
    """Paddle uses ts=TIMESTAMP;h1=HMAC_SHA256 format."""
    parts = dict(item.split("=", 1) for item in signature_header.split(";"))
    ts    = parts.get("ts", "")
    h1    = parts.get("h1", "")

    # Защита от replay-атак: отклоняем события старше 5 минут
    if abs(time.time() - int(ts)) > 300:
        return False

    signed_payload = f"{ts}:{payload.decode()}"
    expected = hmac.new(
        PADDLE_WEBHOOK_SECRET.encode(), signed_payload.encode(), hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, h1)

@app.route("/paddle/webhook", methods=["POST"])
def paddle_webhook():
    sig  = request.headers.get("Paddle-Signature", "")
    body = request.get_data()

    if not verify_paddle_signature(body, sig):
        abort(401)

    event = request.json
    etype = event.get("event_type", "")

    if etype == "transaction.completed":
        handle_payment(event["data"])
    elif etype == "subscription.activated":
        handle_subscription_activated(event["data"])
    elif etype == "subscription.canceled":
        handle_subscription_canceled(event["data"])

    return "ok", 200

Шаг 2 - закрытие сделки при оплате:

import requests

KOMMO_BASE = f"https://{KOMMO_DOMAIN}/api/v4"

def get_kommo_headers():
    return {
        "Authorization": f"Bearer {KOMMO_TOKEN}",
        "Content-Type":  "application/json"
    }

def handle_payment(txn: dict):
    custom_data = txn.get("custom_data") or {}
    lead_id     = custom_data.get("kommo_lead_id")

    if not lead_id:
        # Нет lead_id - искать по email клиента
        email = txn.get("customer", {}).get("email", "")
        lead_id = find_lead_by_email(email)

    if not lead_id:
        return  # не удалось идентифицировать сделку

    # Извлекаем данные платежа
    amount      = txn.get("details", {}).get("totals", {}).get("total", "0")
    currency    = txn.get("currency_code", "USD")
    invoice_num = txn.get("invoice_number", "")
    paddle_txn  = txn.get("id", "")
    items       = txn.get("items", [])
    plan_name   = items[0].get("price", {}).get("name", "") if items else ""

    # Обновляем сделку: статус won + кастомные поля
    hs = requests.Session()
    hs.headers.update(get_kommo_headers())

    hs.patch(f"{KOMMO_BASE}/leads", json=[{
        "id":     int(lead_id),
        "status_id": 142,       # 142 = Успешно реализовано (won)
        "sale":   int(float(amount) * 100),  # Kommo принимает в копейках/центах
        "custom_fields_values": [
            {"field_code": "PADDLE_TXN_ID",    "values": [{"value": paddle_txn}]},
            {"field_code": "INVOICE_NUMBER",   "values": [{"value": invoice_num}]},
            {"field_code": "PLAN_NAME",        "values": [{"value": plan_name}]},
            {"field_code": "PAYMENT_CURRENCY", "values": [{"value": currency}]},
        ]
    }])

    # Добавляем заметку с деталями
    hs.post(f"{KOMMO_BASE}/leads/notes", json=[{
        "entity_id":  int(lead_id),
        "note_type":  "common",
        "params":     {
            "text": (
                f"Оплата Paddle подтверждена\n"
                f"Сумма: {amount} {currency}\n"
                f"Инвойс: {invoice_num}\n"
                f"Тариф: {plan_name}\n"
                f"Transaction ID: {paddle_txn}"
            )
        }
    }])

Шаг 3 - обработка жизненного цикла подписки:

def handle_subscription_activated(sub: dict):
    """Подписка активирована - обновляем поля в Kommo."""
    custom_data = sub.get("custom_data") or {}
    lead_id     = custom_data.get("kommo_lead_id")
    if not lead_id:
        return

    sub_id      = sub.get("id", "")
    next_charge = sub.get("next_billed_at", "")[:10]  # YYYY-MM-DD

    hs = requests.Session()
    hs.headers.update(get_kommo_headers())

    hs.patch(f"{KOMMO_BASE}/leads", json=[{
        "id":    int(lead_id),
        "custom_fields_values": [
            {"field_code": "PADDLE_SUB_ID",      "values": [{"value": sub_id}]},
            {"field_code": "NEXT_BILLING_DATE",  "values": [{"value": next_charge}]},
            {"field_code": "SUBSCRIPTION_STATUS","values": [{"value": "active"}]},
        ]
    }])

def handle_subscription_canceled(sub: dict):
    """Подписка отменена - создаём задачу для менеджера."""
    custom_data = sub.get("custom_data") or {}
    lead_id     = custom_data.get("kommo_lead_id")
    if not lead_id:
        return

    responsible_id = int(custom_data.get("kommo_responsible_id", 0)) or None
    cancel_reason  = sub.get("scheduled_change", {}).get("reason", "customer_request")

    hs = requests.Session()
    hs.headers.update(get_kommo_headers())

    hs.patch(f"{KOMMO_BASE}/leads", json=[{
        "id":    int(lead_id),
        "custom_fields_values": [
            {"field_code": "SUBSCRIPTION_STATUS", "values": [{"value": "canceled"}]},
        ]
    }])

    # Задача менеджеру на работу с оттоком
    hs.post(f"{KOMMO_BASE}/tasks", json=[{
        "task_type_id":   1,       # звонок
        "entity_type":    "leads",
        "entity_id":      int(lead_id),
        "responsible_user_id": responsible_id,
        "text":           f"Клиент отменил подписку Paddle. Причина: {cancel_reason}. Проработать возврат.",
        "complete_till":  int(time.time()) + 86400,  # дедлайн +24 часа
    }])

PADDLE_TXN_ID, INVOICE_NUMBER, PLAN_NAME - это кастомные поля в Kommo, которые нужно создать заранее в Настройки -> Поля -> Сделки. field_code задаётся при создании поля.

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

B2B SaaS-компания: 3 плана (Starter, Pro, Enterprise), цикл продажи 2-4 недели, команда 4 менеджера. До интеграции: каждая оплата в Paddle обрабатывалась вручную - менеджер проверял Paddle Dashboard, менял статус сделки, создавал заметку. В среднем 20-30 минут на сделку, ошибки при передаче данных.

После внедрения кастомного webhook:

  • Сделка закрывается автоматически в течение 3-5 секунд после оплаты
  • Тариф, сумма и Invoice Number записываются в поля сделки без ручного ввода
  • Отмена подписки создаёт задачу на ответственного менеджера
  • 0 пропущенных оплат за 4 месяца эксплуатации

Время реализации: 2 дня. custom_data с kommo_lead_id устраняет главную сложность - идентификацию сделки по факту оплаты.

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

SaaS-компании с подпиской и sales-воронкой в Kommo: продукт продают менеджеры, оплата проходит через Paddle. Если у вас pure self-serve без sales-процесса - интеграция с CRM менее критична. Если есть sales-assisted модель (SDR/AE ведут лид до оплаты) - эта связка обязательна.

Тот же подход применим к другим кастомным интеграциям для Kommo с платёжными платформами. Схожая реализация описана для Kommo + FastSpring - другого MoR с фокусом на enterprise B2B.

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

В чём разница Paddle и FastSpring для SaaS?

Оба являются Merchant of Record и берут на себя НДС. FastSpring традиционно сильнее в enterprise B2B с кастомными ценами и quote-based продажами. Paddle лидирует среди developer-led компаний: современный API, лучший developer experience, сильнее в PLG-моделях. По состоянию на 2026 год Paddle занимает большую долю среди devtools и API-first стартапов, FastSpring - среди зрелых B2B SaaS.

Нужно ли использовать Paddle.js или можно API?

Для передачи custom_data с kommo_lead_id - оба варианта работают. Paddle.js (Checkout) удобен для self-serve flow. Для sales-assisted модели правильнее создавать checkout-сессию через POST /transactions в Paddle API и передавать клиенту ссылку - тогда custom_data формируется на сервере и содержит актуальный ID сделки.

Как обрабатывать возврат платежа (refund) в Kommo?

Paddle отправляет transaction.updated с status: refunded при возврате. В нашей логике добавляем обработчик этого события: создаём заметку в Kommo с суммой возврата, меняем статус кастомного поля на refunded. Сделку не трогаем - она уже закрыта как won.

Как работает мультиподписка (несколько продуктов)?

Если клиент покупает несколько продуктов - в transaction.items будет несколько объектов. Все планы записываем в PLAN_NAME через запятую или создаём отдельную сделку на каждый продукт - зависит от вашей структуры воронки в Kommo.

Можно ли работать с Paddle в тестовом режиме?

Да. Paddle имеет sandbox environment на sandbox.paddle.com. Webhook-события с sandbox идут на тот же endpoint, в payload добавляется "test": true. В нашей реализации проверяем это поле и логируем события без записи в Kommo - удобно для отладки.

Итог

Kommo + Paddle интеграция строится на одном ключевом механизме: custom_data с kommo_lead_id передаётся при инициализации Paddle Checkout и доступен в каждом webhook-событии. Это делает привязку платежа к сделке однозначной без поиска по email.

Схема:

  • Paddle Checkout с custom_data: { kommo_lead_id } при переходе к оплате
  • Webhook transaction.completed -> Kommo: сделка в статус won, поля заполнены
  • Webhook subscription.activated -> Kommo: дата следующего списания, Sub ID
  • Webhook subscription.canceled -> Kommo: задача менеджеру на работу с оттоком

Если ваша команда ведёт продажи SaaS через Kommo и использует Paddle для приёма оплаты - опишите задачу команде Exceltic.dev. Разберём архитектуру и настроим автоматическое закрытие сделок.

Ещё статьи

Все →