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 и далее по жизненному циклу подписки.
Архитектура интеграции
Поток данных:
- Менеджер ведёт сделку в Kommo до этапа «Оплата»
- Генерируется Paddle Checkout с
kommo_lead_idвcustom_data - Клиент оплачивает - Paddle отправляет
transaction.completedwebhook - Наш сервис обновляет сделку в Kommo: статус, поля, заметка
- При отмене подписки - 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. Разберём архитектуру и настроим автоматическое закрытие сделок.