Kommo + Paddle: автоматическое создание подписок через Merchant of Record

Kommo + Paddle: автоматическое создание подписок через Merchant of Record

Paddle — не просто платёжный шлюз, а Merchant of Record: он берёт на себя начисление и уплату EU VAT, UK VAT, US sales tax и GST по всему миру. Без интеграции с Kommo менеджер закрывает сделку, затем вручную открывает Paddle и создаёт клиента и подписку — с задержкой и риском ошибки в данных. С интеграцией Won в Kommo автоматически запускает Paddle-подписку, а события оплаты и отмены мгновенно появляются в карточке сделки.

Почему Paddle отличается от Stripe и Chargebee

Merchant of Record (MoR) — модель, при которой платёжный провайдер юридически выступает продавцом в транзакции. Paddle выставляет счёт клиенту от своего имени, собирает налог и сам перечисляет его в налоговые органы.

Для SaaS-компаний, продающих в ЕС, Великобритании или США, это снимает три проблемы:
EU VAT: Paddle регистрирует НДС в каждой стране ЕС самостоятельно — вам не нужна регистрация в 27 странах
US sales tax: Paddle отслеживает nexus и перечисляет tax по штатам
Compliance: инвойсы с корректным VAT-номером Paddle, а не вашей компании

В сравнении с подпиской через Chargebee, где налоговая ответственность остаётся на вас, Paddle полностью снимает этот вопрос.

Это критично для продукта с клиентами в разных юрисдикциях — и именно поэтому Paddle выбирают B2B SaaS при выходе на международный рынок.

Что синхронизируется между Kommo и Paddle

Kommo -> Paddle:
— Won -> создать Paddle customer (email, имя, страна из полей сделки)
— Won -> создать Paddle subscription (price_id из кастомного поля «Тариф»)
— Обновление контакта (email/имя) -> обновить Paddle customer

Paddle -> Kommo:
subscription.created -> Note: «Paddle: подписка активирована, ID = sub_xxx»
transaction.completed -> Note: «Paddle: платёж $X за период»
transaction.payment_failed -> Note + задача менеджеру: «Paddle: платёж не прошёл, уточнить данные карты»
subscription.canceled -> Note + кастомное поле paddle_status = canceled
subscription.paused -> Note + поле paddle_status = paused

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

Kommo Webhook: сделка перешла в Won
  ↓ Backend
  1. GET /api/v4/leads/{id} + contacts
     -> email, имя, страна, тариф из кастомного поля
  2. Paddle API: POST /customers
     -> создать или обновить клиента
  3. Paddle API: POST /subscriptions
     -> price_id = маппинг тарифа, customer_id из шага 2
     -> collection_mode = "automatic" (автооплата)
  4. Kommo: POST /leads/{id}/notes
     -> «Paddle: подписка {subscription_id} активирована»

Paddle Webhook: transaction.payment_failed
  ↓ Backend
  1. Верификация HMAC-SHA256 (Paddle-Signature header)
  2. Извлечь customer.id -> найти сделку по полю paddle_customer_id
  3. Kommo: POST /leads/{deal_id}/notes -> «Paddle: платёж не прошёл»
  4. Kommo: POST /tasks -> «Уточнить платёжные данные клиента в Paddle»

Paddle API: ключевые запросы

Base URL: https://api.paddle.com. Sandbox: https://sandbox-api.paddle.com.
Аутентификация: Bearer token (Authorization: Bearer {api_key}).

Создать клиента в Paddle:

import requests

PADDLE_API_KEY = "your_paddle_api_key"
PADDLE_BASE_URL = "https://api.paddle.com"

headers = {
    "Authorization": f"Bearer {PADDLE_API_KEY}",
    "Content-Type": "application/json"
}

def create_paddle_customer(email: str, name: str, country_code: str) -> dict:
    resp = requests.post(
        f"{PADDLE_BASE_URL}/customers",
        headers=headers,
        json={
            "email": email,
            "name": name,
            "locale": "en",
            "custom_data": {"country": country_code}
        }
    )
    resp.raise_for_status()
    return resp.json()["data"]

Создать подписку:

PLAN_TO_PRICE_ID = {
    "starter": "pri_01abc123",
    "growth":  "pri_01def456",
    "scale":   "pri_01ghi789",
}

def create_paddle_subscription(customer_id: str, plan: str) -> dict:
    price_id = PLAN_TO_PRICE_ID.get(plan)
    if not price_id:
        raise ValueError(f"Unknown plan: {plan}")

    resp = requests.post(
        f"{PADDLE_BASE_URL}/subscriptions",
        headers=headers,
        json={
            "customer_id": customer_id,
            "items": [{"price_id": price_id, "quantity": 1}],
            "collection_mode": "automatic",
        }
    )
    resp.raise_for_status()
    return resp.json()["data"]

def on_deal_won(lead: dict, contact: dict):
    email = get_contact_email(contact)
    name = contact["name"]
    plan = get_custom_field(lead, PLAN_FIELD_ID) or "starter"
    country = get_custom_field(lead, COUNTRY_FIELD_ID) or "US"

    customer = create_paddle_customer(email, name, country)
    subscription = create_paddle_subscription(customer["id"], plan)

    # Сохранить paddle_customer_id в Kommo для обратного поиска
    update_kommo_deal(lead["id"], {
        "paddle_customer_id": customer["id"],
        "paddle_subscription_id": subscription["id"],
        "paddle_status": "active"
    })
    create_kommo_note(
        lead["id"],
        f"Paddle: подписка {subscription['id']} активирована (тариф {plan})"
    )

Обработка Paddle Webhook с верификацией HMAC:

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

app = Flask(__name__)
PADDLE_WEBHOOK_SECRET = "your_webhook_secret"

def verify_paddle_signature(payload: bytes, signature_header: str) -> bool:
    # Paddle-Signature: ts=1234567890;h1=abc123...
    parts = dict(part.split("=", 1) for part in signature_header.split(";"))
    ts = parts.get("ts", "")
    h1 = parts.get("h1", "")
    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("/webhooks/paddle", methods=["POST"])
def paddle_webhook():
    signature = request.headers.get("Paddle-Signature", "")
    if not verify_paddle_signature(request.data, signature):
        abort(403)

    payload = request.json
    event_type = payload.get("event_type")
    event_id = payload.get("event_id")  # idempotency key
    data = payload.get("data", {})

    if event_type == "transaction.payment_failed":
        customer_id = data.get("customer_id")
        amount = data.get("details", {}).get("totals", {}).get("total", "?")
        currency = data.get("currency_code", "USD")
        deal_id = find_deal_by_paddle_customer(customer_id)
        if deal_id:
            create_kommo_note(deal_id,
                f"Paddle: платёж не прошёл (сумма {amount} {currency})")
            create_kommo_task(deal_id,
                "Уточнить платёжные данные - Paddle зафиксировал неудачный платёж")

    elif event_type == "subscription.canceled":
        customer_id = data.get("customer_id")
        deal_id = find_deal_by_paddle_customer(customer_id)
        if deal_id:
            update_kommo_deal(deal_id, {"paddle_status": "canceled"})
            create_kommo_note(deal_id,
                "Paddle: подписка отменена")

    elif event_type == "transaction.completed":
        customer_id = data.get("customer_id")
        totals = data.get("details", {}).get("totals", {})
        amount = totals.get("total", "?")
        currency = data.get("currency_code", "USD")
        deal_id = find_deal_by_paddle_customer(customer_id)
        if deal_id:
            create_kommo_note(deal_id,
                f"Paddle: платёж {amount} {currency} получен")

    return "", 200

Настройка Webhook в Paddle: Paddle Dashboard -> Notifications -> New destination. Указать URL, выбрать события. Secret key генерируется автоматически — скопировать в PADDLE_WEBHOOK_SECRET.

Налоговая логика Paddle: что это означает на практике

Paddle автоматически определяет налоговую ставку по IP и billing country клиента. Для бизнес-клиентов из ЕС — запрашивает VAT-номер и применяет reverse charge. Для физлиц в ЕС — начисляет НДС по ставке страны клиента.

В инвойсе указывается Paddle как продавец. Клиент получает корректный налоговый документ без вашего участия. Для Kommo это означает: поле «тариф» в сделке -> подписка создана -> Paddle сам разбирается с налогами.

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

B2B SaaS (Европа, 40–60 новых клиентов в месяц, Kommo + Paddle + Stripe был заменён на Paddle как MoR):

  • До: при каждом Won менеджер вручную открывал Paddle, создавал клиента, выбирал план. Задержка 30–90 минут, иногда — ошибочный тариф в подписке. Налоговые вопросы от EU-клиентов по VAT закрывал финдиректор вручную.
  • После: Won -> подписка в Paddle за 8 секунд. Тариф берётся из кастомного поля сделки — ошибок нет. EU VAT автоматически. При payment_failed — задача менеджеру в тот же день.
  • Дополнительно: subscription.canceled -> кастомное поле + задача CSM -> retention-аутрич в течение 24 часов вместо «случайно узнали через неделю».

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

  • SaaS с клиентами в ЕС, UK, США — там, где нужен корректный VAT/sales tax на инвойсе
  • Команды, уже работающие с Kommo как основной CRM и выбирающие Paddle как MoR
  • Продукты с несколькими тарифами — чтобы тариф из сделки автоматически попадал в Paddle без ручного выбора
  • Компании, которые хотят видеть историю платежей в карточке клиента без переключения между системами

Для оценки кастомных интеграций Kommo с платёжными системами — объём работы обычно 1–2 недели: маппинг тарифов, обработка webhook, хранение paddle_customer_id.

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

Чем Paddle отличается от Stripe Billing?

Stripe — payment processor: вы отвечаете за налоги сами. Paddle — Merchant of Record: Paddle юридически продавец, он собирает и перечисляет налоги. Для компаний с EU-клиентами это принципиально: Stripe требует вашей регистрации НДС в ЕС или подключения TaxJar/Avalara, Paddle делает это сам. Kommo + Stripe интеграция описана отдельно.

Paddle поддерживает разовые платежи или только подписки?

Оба варианта. transaction.completed приходит и для разовых платежей (payment links), и для recurring. Для Kommo-интеграции разовый платёж обрабатывается аналогично: находим сделку по customer_id и пишем Note.

Как хранить paddle_customer_id в Kommo?

Создайте кастомное поле типа «Текст» в настройках Kommo -> Сделки -> Поля. При создании клиента в Paddle сразу записывайте ID через PATCH /api/v4/leads/{id}. Это поле используется для поиска сделки по входящим webhook-событиям.

Что происходит если клиент уже есть в Paddle?

Paddle возвращает ошибку 409 при дублирующемся email. Перед созданием — делайте GET /customers?search={email}. Если клиент найден — обновляйте данные через PATCH /customers/{id} и создавайте новую подписку к существующему клиенту.

Как тестировать без реальных платежей?

Paddle предоставляет sandbox-среду: https://sandbox-api.paddle.com. Sandbox-ключи создаются в Paddle Dashboard -> Developer Tools -> Authentication. Webhook события можно триггерить вручную из Paddle Dashboard в sandbox-режиме.

Итого

  • Paddle: Bearer auth, base URL https://api.paddle.com
  • Создать клиента: POST /customers, создать подписку: POST /subscriptions
  • Хранить paddle_customer_id в кастомном поле Kommo для обратного поиска
  • Webhook верификация: HMAC-SHA256 через Paddle-Signature header, ts:payload формат
  • Ключевые события: subscription.created, transaction.completed, transaction.payment_failed, subscription.canceled
  • MoR-модель: EU VAT, UK VAT, US sales tax — Paddle берёт на себя полностью

Если вы используете Paddle и Kommo и хотите замкнуть подписку на CRM — опишите тарифную сетку и кастомные поля. Exceltic.dev настроит маппинг и обработку webhook-событий.

Ещё статьи

Все →