Kommo + Moosend: email-автоматизация из воронки продаж

Kommo + Moosend: email-автоматизация из воронки

Интеграция Kommo и Moosend через API позволяет автоматически добавлять контакты в email-списки при смене этапа воронки, запускать триггерные automation и синхронизировать custom fields в обе стороны. Никакого ручного CSV-экспорта, никаких задержек между обновлением CRM и рассылкой.

Moosend активно занимает нишу EU-альтернативы Mailchimp после того, как Mailchimp в 2023-2024 годах поднял цены и ужесточил условия для European-аккаунтов. В проектах на Kommo мы видим один и тот же паттерн: контакты из CRM добавляются в email-списки вручную через CSV-экспорт раз в неделю. К моменту отправки рассылки данные уже устарели - сделка закрыта, контакт перешёл в другой сегмент, а письмо всё равно уходит с неверным контекстом. Ниже - архитектура интеграции, рабочий Python-код и кейс с цифрами.

Почему нативная интеграция Kommo и Moosend не решает задачу

Kommo не имеет нативного коннектора к Moosend. Официальная страница интеграций Kommo предлагает MailChimp и несколько других ESP, но Moosend в этом списке отсутствует. Варианты через Zapier или Make работают, но имеют принципиальные ограничения.

Запрос через Zapier срабатывает по webhook-событию от Kommo, но не умеет проверять уже существующий subscriber в Moosend перед добавлением - это создаёт дубли. Make лучше справляется с логикой, но при объёме 500+ обновлений в месяц стоимость операций растёт быстрее, чем полезная нагрузка. Кроме того, ни Zapier, ни Make не дают инструментов для двусторонней синхронизации: если контакт отписался в Moosend, статус в Kommo не изменится.

Кастомная интеграция через API закрывает все эти сценарии: идемпотентное добавление, маппинг custom fields, обратный webhook от Moosend при отписке.

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

Стек: Python 3.11, Kommo Webhooks, Moosend REST API v3, PostgreSQL для журнала синхронизации.

Поток данных (Kommo -> Moosend):

  1. Kommo отправляет webhook при смене этапа воронки (lead.status)
  2. Python-сервер получает событие, извлекает данные лида из Kommo API
  3. Маппинг полей: name, email, custom_fields (industry, deal_value, stage)
  4. POST в Moosend Subscribers API - добавление или обновление subscriber
  5. Запуск automation workflow через Moosend API если нужно

Обратный поток (Moosend -> Kommo):

  1. Moosend отправляет webhook при UnsubscribeEvent
  2. Сервер находит контакт в Kommo по email
  3. Обновляет custom field email_opt_in в значение false
  4. Добавляет заметку в карточку сделки

Auth: Moosend использует API Key в query string параметре apikey. Ключ генерируется в настройках аккаунта: Settings -> API key. Kommo использует Long-lived access token через OAuth2.

import hmac
import hashlib
import logging
import requests
from datetime import datetime, timezone
from flask import Flask, request, jsonify

app = Flask(__name__)
logging.basicConfig(level=logging.INFO)

MOOSEND_API_KEY = "your-moosend-api-key"
MOOSEND_BASE = "https://api.moosend.com/v3"
KOMMO_BASE = "https://your-domain.kommo.com"
KOMMO_TOKEN = "your-kommo-long-lived-token"

# Маппинг этапов воронки Kommo на списки Moosend
STAGE_TO_LIST = {
    142: "mailing-list-id-trial",       # Trial
    143: "mailing-list-id-demo",        # Demo scheduled
    144: "mailing-list-id-negotiation", # Negotiation
    145: "mailing-list-id-won",         # Won
}

def get_kommo_lead(lead_id: int) -> dict:
    """Получить данные лида из Kommo API."""
    url = f"{KOMMO_BASE}/api/v4/leads/{lead_id}?with=contacts,custom_fields_values"
    headers = {"Authorization": f"Bearer {KOMMO_TOKEN}"}
    resp = requests.get(url, headers=headers, timeout=10)
    resp.raise_for_status()
    return resp.json()

def get_contact_email(contact_id: int) -> str | None:
    """Получить email контакта из Kommo."""
    url = f"{KOMMO_BASE}/api/v4/contacts/{contact_id}?with=custom_fields_values"
    headers = {"Authorization": f"Bearer {KOMMO_TOKEN}"}
    resp = requests.get(url, headers=headers, timeout=10)
    resp.raise_for_status()
    data = resp.json()
    for field in data.get("custom_fields_values", []):
        if field["field_code"] == "EMAIL":
            return field["values"][0]["value"]
    return None

def subscribe_to_moosend(
    list_id: str,
    email: str,
    name: str,
    custom_fields: dict
) -> dict:
    """
    Добавить или обновить subscriber в Moosend.
    POST /v3/subscribers/{MailingListID}/subscribe.json
    Если subscriber уже есть - Moosend выполнит update (upsert-семантика).
    """
    url = f"{MOOSEND_BASE}/subscribers/{list_id}/subscribe.json"
    payload = {
        "Name": name,
        "Email": email,
        "CustomFields": [
            f"{k}={v}" for k, v in custom_fields.items()
        ],
        "HasExternalDoubleOptIn": False  # double opt-in обрабатывается Moosend
    }
    resp = requests.post(
        url,
        json=payload,
        params={"apikey": MOOSEND_API_KEY},
        timeout=10
    )
    resp.raise_for_status()
    return resp.json()

def update_kommo_contact_field(contact_id: int, field_id: int, value: str):
    """Обновить custom field контакта в Kommo."""
    url = f"{KOMMO_BASE}/api/v4/contacts/{contact_id}"
    headers = {
        "Authorization": f"Bearer {KOMMO_TOKEN}",
        "Content-Type": "application/json"
    }
    payload = {
        "custom_fields_values": [
            {"field_id": field_id, "values": [{"value": value}]}
        ]
    }
    resp = requests.patch(url, json=payload, headers=headers, timeout=10)
    resp.raise_for_status()

@app.route("/kommo/webhook", methods=["POST"])
def kommo_webhook():
    """Webhook от Kommo: смена этапа воронки."""
    data = request.json
    leads = data.get("leads", {}).get("status", [])

    for lead_event in leads:
        lead_id = lead_event.get("id")
        status_id = lead_event.get("status_id")

        if status_id not in STAGE_TO_LIST:
            continue

        list_id = STAGE_TO_LIST[status_id]

        try:
            lead_data = get_kommo_lead(lead_id)
            lead = lead_data

            # Получить email из первого контакта сделки
            contacts = lead.get("_embedded", {}).get("contacts", [])
            if not contacts:
                logging.warning(f"Lead {lead_id}: no contacts")
                continue

            contact_id = contacts[0]["id"]
            email = get_contact_email(contact_id)
            if not email:
                logging.warning(f"Contact {contact_id}: no email")
                continue

            # Маппинг custom fields из Kommo в Moosend
            custom_fields = {}
            for cf in lead.get("custom_fields_values", []):
                if cf["field_code"] == "INDUSTRY":
                    custom_fields["Industry"] = cf["values"][0]["value"]
                elif cf["field_id"] == 123456:  # Deal Value field ID
                    custom_fields["DealValue"] = str(cf["values"][0]["value"])

            custom_fields["KommoStage"] = str(status_id)
            custom_fields["LastSync"] = datetime.now(timezone.utc).isoformat()

            result = subscribe_to_moosend(
                list_id=list_id,
                email=email,
                name=lead.get("name", ""),
                custom_fields=custom_fields
            )
            logging.info(f"Synced lead {lead_id} to Moosend list {list_id}: {result}")

        except requests.HTTPError as e:
            logging.error(f"HTTP error for lead {lead_id}: {e.response.status_code} {e.response.text}")
        except Exception as e:
            logging.exception(f"Unexpected error for lead {lead_id}: {e}")

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

@app.route("/moosend/webhook", methods=["POST"])
def moosend_unsubscribe_webhook():
    """
    Webhook от Moosend при отписке (UnsubscribeEvent).
    Обновляет custom field в Kommo.
    """
    data = request.json
    event_type = data.get("EventType")
    if event_type != "UnsubscribeEvent":
        return jsonify({"status": "ignored"})

    email = data.get("Email")
    if not email:
        return jsonify({"status": "no_email"}), 400

    # Найти контакт в Kommo по email
    search_url = f"{KOMMO_BASE}/api/v4/contacts?query={email}"
    headers = {"Authorization": f"Bearer {KOMMO_TOKEN}"}
    resp = requests.get(search_url, headers=headers, timeout=10)
    contacts = resp.json().get("_embedded", {}).get("contacts", [])

    if not contacts:
        logging.warning(f"Unsubscribe: contact not found for {email}")
        return jsonify({"status": "not_found"})

    contact_id = contacts[0]["id"]
    # field_id 654321 - ID вашего custom field email_opt_in в Kommo
    update_kommo_contact_field(contact_id, field_id=654321, value="false")
    logging.info(f"Marked opt-out in Kommo for contact {contact_id} ({email})")

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

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080)

Пошаговая реализация

Шаг 1. Настройка Moosend

В аккаунте Moosend: Settings -> API key -> копируете ключ. Далее создаёте мэйлинг-листы под каждый этап воронки. В Custom Fields Moosend добавляете поля: Industry, DealValue, KommoStage, LastSync. Для GDPR-компланс активируйте double opt-in на уровне списка - Moosend отправит подтверждающее письмо автоматически при первом добавлении.

Шаг 2. Настройка webhook в Kommo

Kommo Settings -> Webhooks -> Add webhook. URL: https://your-server.com/kommo/webhook. События: Lead status changed. Kommo отправляет JSON с leads.status массивом при каждой смене этапа.

Шаг 3. Маппинг полей

Moosend Custom Fields принимают данные в формате "FieldName=Value" в массиве CustomFields. Имена полей в Moosend должны совпадать с тем, что вы создали в настройках списка. Числовые значения (deal value) передаются как строки.

Шаг 4. Webhook Moosend для обратной синхронизации

Moosend Settings -> Integrations -> Webhooks -> Добавить URL вашего сервера для события Unsubscribe. При отписке Moosend отправит POST с EventType: "UnsubscribeEvent" и Email. Сервер находит контакт в Kommo и обновляет поле opt-in.

Шаг 5. Обработка ошибок и идемпотентность

Moosend Subscribe endpoint имеет upsert-семантику: если subscriber с таким email уже есть в списке, запрос выполнит обновление. Дублей не возникает. При ошибке 429 (rate limit) используйте exponential backoff: первый retry через 1 секунду, второй через 4, третий через 16. Логируйте все ошибки с lead_id для ретроспективной проверки.

Реальный кейс с цифрами

B2B SaaS-компания в Нидерландах, 25 сотрудников, 600+ активных лидов в Kommo. До интеграции: менеджер по маркетингу тратил 2-3 часа в неделю на ручной экспорт CSV из Kommo и загрузку в Moosend. Данные в рассылке опаздывали на 3-7 дней. Около 15% писем уходило контактам, которые уже перешли в другой этап воронки или вовсе были закрыты.

После запуска интеграции: задержка синхронизации - менее 30 секунд. Open rate вырос с 21% до 29% за первый месяц - письма стали уходить с корректным контекстом. Время на ручную работу - 0. Дополнительно: при смене статуса на Won контакт автоматически переносится в onboarding-список Moosend и получает welcome-последовательность через automation Moosend.

GDPR: все новые контакты проходят double opt-in. Timestamp подтверждения хранится в Moosend и доступен через API при необходимости аудита.

Для кого подходит

Интеграция актуальна для B2B-компаний в EU с 200+ активными лидами в Kommo, которые используют email как основной канал прогрева. Особенно эффективна для SaaS (trial -> demo -> won сегментация), агентств с нескольким этапами онбординга и e-commerce с повторными покупками. Если вы уже рассматривали кастомные интеграции Kommo как альтернативу Zapier - Moosend через API это типичный кейс такой замены.

Moosend подходит командам, которым важны GDPR-compliance из коробки, EU-хостинг данных и более предсказуемая структура цен по сравнению с Mailchimp. Для сравнения с другими ESP: статья Kommo + Mailchimp: синхронизация контактов разбирает схожую архитектуру для Mailchimp; Kommo + Customer.io: триггерные email из воронки - более сложный event-driven подход для SaaS.

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

Как Moosend аутентифицирует запросы к API?

Moosend использует API Key как query-параметр apikey во всех запросах к REST API v3. Ключ генерируется в настройках аккаунта: Settings -> API key. Альтернативно, ключ можно передавать в заголовке запроса. Ключ имеет полный доступ к аккаунту, поэтому храните его в переменных окружения, не в коде. Документация: docs.moosend.com.

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

Endpoint POST /v3/subscribers/{MailingListID}/subscribe.json имеет upsert-семантику. Если subscriber с указанным email уже существует в списке, Moosend выполнит обновление его данных (имя, custom fields) вместо создания дубля. Это безопасно вызывать при каждом изменении в Kommo - повторных подписчиков не появится.

Как настроить GDPR double opt-in в связке с Kommo?

Double opt-in включается на уровне мэйлинг-листа в Moosend: List Settings -> Confirmation Email -> Enable. После этого при добавлении нового subscriber через API Moosend автоматически отправит подтверждающее письмо. Subscriber активируется только после клика. В коде установите HasExternalDoubleOptIn: false если вы хотите чтобы Moosend сам управлял opt-in процессом. Timestamp подтверждения доступен через Moosend API для GDPR-аудита.

Как синхронизировать отписки из Moosend обратно в Kommo?

Moosend поддерживает исходящие webhooks: Settings -> Integrations -> Webhooks. Настройте URL вашего сервера для события Unsubscribe. При отписке Moosend отправит POST-запрос с email адресом. Сервер ищет контакт в Kommo по email через GET /api/v4/contacts?query=email и обновляет custom field (например, email_opt_in = false). Это важно для GDPR: вы обязаны прекратить email-коммуникации если контакт отписался.

Можно ли запускать Moosend automation из Kommo?

Да. Moosend Automation поддерживает триггер Custom Event - вы можете отправить событие через API и запустить automation workflow. Endpoint: POST /v3/subscribers/{MailingListID}/trigger.json с указанием названия события. Например, при переходе лида на этап Demo scheduled в Kommo можно запустить automation demo-reminder-sequence в Moosend, которая отправит серию писем с подготовкой к встрече.

Что дальше

Если у вас 200+ лидов в Kommo и email как основной канал прогрева - задержка в несколько дней между обновлением CRM и рассылкой напрямую влияет на конверсию. Это решается за 1-2 недели разработки.

Опишите вашу задачу команде Exceltic.dev: какой ESP используете, какие этапы воронки нужно синхронизировать, есть ли требования по GDPR. Разберём архитектуру и дадим оценку по объёму работ.

Ещё статьи

Все →