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):
- Kommo отправляет webhook при смене этапа воронки (
lead.status) - Python-сервер получает событие, извлекает данные лида из Kommo API
- Маппинг полей:
name,email,custom_fields(industry, deal_value, stage) - POST в Moosend Subscribers API - добавление или обновление subscriber
- Запуск automation workflow через Moosend API если нужно
Обратный поток (Moosend -> Kommo):
- Moosend отправляет webhook при
UnsubscribeEvent - Сервер находит контакт в Kommo по email
- Обновляет custom field
email_opt_inв значениеfalse - Добавляет заметку в карточку сделки
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. Разберём архитектуру и дадим оценку по объёму работ.