Kommo + MailerLite: email-маркетинг из воронки без Zapier и дублей контактов

MailerLite - email-маркетинг с простым тарифным планом, GDPR-compliant инфраструктурой и REST API v2. Для B2B команд интеграция с Kommo решает конкретную задачу: автоматически добавлять лид в нужную группу MailerLite при переходе между этапами воронки, и синхронизировать статус подписки обратно в Kommo. Без Zapier и без дублей - через кастомную интеграцию.

MailerLite API v2 использует Bearer token (Authorization: Bearer {API_KEY}). Основные операции: POST /api/subscribers - создать/обновить подписчика, POST /api/groups/{id}/subscribers - добавить в группу, DELETE /api/groups/{id}/subscribers/{email} - удалить из группы. Webhooks отправляются при действиях подписчика: открыл письмо, кликнул, отписался.

Группа MailerLite - список подписчиков, аналог HubSpot List или Mailchimp Audience. Каждый этап воронки Kommo соответствует определённой группе MailerLite.

Архитектура: воронка как источник сегментации

Kommo этап "Квалифицирован"
  -> webhook leads.status.changed
  -> Ваш сервер

Ваш сервер
  -> GET Kommo: email и имя контакта по сделке
  -> MailerLite API: upsert subscriber + поле lead_id
  -> MailerLite API: добавить в группу "Квалифицированные лиды"
  -> убрать из предыдущей группы (если была)

MailerLite: отправить nurturing-последовательность
  -> webhook: subscriber.unsubscribed
  -> Ваш сервер -> Kommo: note "Отписался от email"

Реализация: Kommo -> MailerLite

import requests, os
from flask import Flask, request, jsonify

app = Flask(__name__)

ML_API_KEY  = os.environ["MAILERLITE_API_KEY"]
ML_BASE     = "https://connect.mailerlite.com/api"
ML_HDR      = {"Authorization": f"Bearer {ML_API_KEY}", "Content-Type": "application/json"}

KOMMO_SUBDOMAIN = os.environ["KOMMO_SUBDOMAIN"]
KOMMO_TOKEN     = os.environ["KOMMO_ACCESS_TOKEN"]
KOMMO_BASE      = f"https://{KOMMO_SUBDOMAIN}.kommo.com/api/v4"
KOMMO_HDR       = {"Authorization": f"Bearer {KOMMO_TOKEN}", "Content-Type": "application/json"}

# Маппинг этапов Kommo -> группы MailerLite
STAGE_TO_GROUP = {
    int(os.environ.get("STAGE_QUALIFIED",   "0")): os.environ.get("ML_GROUP_QUALIFIED",   ""),
    int(os.environ.get("STAGE_PROPOSAL",    "0")): os.environ.get("ML_GROUP_PROPOSAL",    ""),
    int(os.environ.get("STAGE_NEGOTIATION", "0")): os.environ.get("ML_GROUP_NEGOTIATION", ""),
    int(os.environ.get("STAGE_WON",         "0")): os.environ.get("ML_GROUP_WON",         ""),
    int(os.environ.get("STAGE_LOST",        "0")): os.environ.get("ML_GROUP_LOST",        ""),
}

def get_contact_for_lead(lead_id: int) -> tuple[str, str, str]:
    r = requests.get(
        f"{KOMMO_BASE}/leads/{lead_id}",
        headers=KOMMO_HDR,
        params={"with": "contacts"},
    )
    contacts = r.json().get("_embedded", {}).get("contacts", [])
    if not contacts:
        return "", "", ""
    rc = requests.get(
        f"{KOMMO_BASE}/contacts/{contacts[0]['id']}",
        headers=KOMMO_HDR,
        params={"with": "custom_fields_values"},
    )
    c     = rc.json()
    email = ""
    for cf in c.get("custom_fields_values", []) or []:
        if cf.get("field_code") == "EMAIL":
            vals = cf.get("values", [])
            if vals:
                email = vals[0].get("value", "")
                break
    name  = c.get("name", "")
    parts = name.split(maxsplit=1)
    first = parts[0] if parts else ""
    last  = parts[1] if len(parts) > 1 else ""
    return email, first, last

def upsert_subscriber(email: str, first: str, last: str, lead_id: int) -> dict:
    r = requests.post(
        f"{ML_BASE}/subscribers",
        headers=ML_HDR,
        json={
            "email":  email,
            "fields": {
                "name":    first,
                "last_name": last,
                "kommo_lead_id": str(lead_id),
            },
            "status": "active",
        },
    )
    r.raise_for_status()
    return r.json().get("data", {})

def add_to_group(subscriber_id: str, group_id: str):
    requests.post(
        f"{ML_BASE}/subscribers/{subscriber_id}/groups",
        headers=ML_HDR,
        json={"groups": [group_id]},
    )

def remove_from_all_groups(subscriber_id: str, exclude_group: str):
    r = requests.get(f"{ML_BASE}/subscribers/{subscriber_id}/groups", headers=ML_HDR)
    for g in r.json().get("data", []) or []:
        gid = g.get("id", "")
        if gid and gid != exclude_group:
            requests.delete(
                f"{ML_BASE}/subscribers/{subscriber_id}/groups/{gid}",
                headers=ML_HDR,
            )

def add_note(lead_id: int, text: str):
    requests.post(
        f"{KOMMO_BASE}/notes",
        headers=KOMMO_HDR,
        json=[{
            "entity_id":   lead_id,
            "entity_type": "leads",
            "note_type":   "common",
            "params":      {"text": text},
        }],
    )

@app.route("/webhooks/kommo", methods=["POST"])
def kommo_webhook():
    data = request.json or {}
    for lead_data in data.get("leads", {}).get("status", []):
        lead_id    = lead_data.get("id")
        new_status = lead_data.get("status_id")
        group_id   = STAGE_TO_GROUP.get(new_status)
        if not group_id:
            continue

        email, first, last = get_contact_for_lead(lead_id)
        if not email:
            continue

        sub      = upsert_subscriber(email, first, last, lead_id)
        sub_id   = sub.get("id", "")
        if not sub_id:
            continue

        remove_from_all_groups(sub_id, group_id)
        add_to_group(sub_id, group_id)
        add_note(lead_id, f"MailerLite: подписчик {email} добавлен в группу {group_id}.")

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

Реализация: MailerLite webhook -> Kommo

@app.route("/webhooks/mailerlite", methods=["POST"])
def mailerlite_webhook():
    # MailerLite подписывает webhooks через X-Mailerlite-Signature
    event = request.json or {}
    ev    = event.get("event", {})
    ev_type = ev.get("type", "")

    if ev_type not in ("subscriber.unsubscribed", "subscriber.bounced", "subscriber.junk"):
        return jsonify({"status": "ignored"}), 200

    subscriber = event.get("subscriber", {}) or {}
    fields     = subscriber.get("fields", {}) or {}
    lead_id    = (fields.get("kommo_lead_id") or {}).get("value", "")
    email      = subscriber.get("email", "")

    if not lead_id:
        return jsonify({"status": "no_lead_id"}), 200

    msgs = {
        "subscriber.unsubscribed": f"MailerLite: {email} отписался от рассылки.",
        "subscriber.bounced":      f"MailerLite: email {email} недоступен (bounce).",
        "subscriber.junk":         f"MailerLite: {email} пометил письмо как спам.",
    }
    add_note(int(lead_id), msgs.get(ev_type, f"MailerLite: событие {ev_type} для {email}."))
    return jsonify({"status": "ok"}), 200

Настройка webhook MailerLite

Dashboard -> Settings -> Webhooks -> Add webhook. URL: https://your-server.com/webhooks/mailerlite. Events: выбрать все subscriber-события.

MailerLite подписывает тело запроса через X-Mailerlite-Signature. Алгоритм: HMAC-SHA256 тела с webhook secret. Добавьте верификацию аналогично примеру выше.

Поле kommo_lead_id в MailerLite

При создании подписчика через API поле kommo_lead_id передаётся в fields. MailerLite хранит кастомные поля для каждого подписчика. При настройке: создайте кастомное поле kommo_lead_id (тип Text) в MailerLite Dashboard -> Settings -> Subscriber fields.

Предотвращение дублей

MailerLite использует email как уникальный ключ - POST /api/subscribers с существующим email обновит запись, не создаст дубль. Это работает корректно только если в Kommo у контактов нет нескольких email-адресов. Используйте основной (первый) email из field_code: EMAIL.

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

B2B команды с CRM-воронкой в Kommo и email-маркетингом в MailerLite. Особенно полезно если:

  • Разные стадии сделки должны получать разные nurturing-цепочки
  • Нужно останавливать рассылку при потере интереса (lead = Lost -> unsubscribe)
  • Команда не хочет мониторить два инструмента отдельно

Аналогичные интеграции описаны для Kommo + Ortto и Kommo + ActiveCampaign.

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

Как убедиться что подписчик не получит два письма из разных групп?

MailerLite отправляет automation только для группы, которая её тригерит. Если подписчик в двух группах, он получает оба automation. Решение: использовать remove_from_all_groups перед добавлением в новую - гарантирует что подписчик всегда только в одной Kommo-группе.

Работает ли MailerLite API v1 с этим кодом?

Нет. MailerLite v1 устарел и не поддерживается. Используйте v2 с базовым URL https://connect.mailerlite.com/api. API Key в v2 создаётся через Dashboard -> Settings -> Developer API.

Как сегментировать по стране через MailerLite из Kommo?

Добавьте поле country в fields при создании подписчика. Значение берите из custom field контакта в Kommo. Затем в MailerLite создайте Segment с условием fields.country = "US" - используйте его для гео-таргетинга кампаний.

Итог

Kommo + MailerLite - email-маркетинг из воронки:

  • Bearer token, POST /api/subscribers - upsert по email (без дублей)
  • kommo_lead_id в кастомном поле для обратной корреляции
  • remove_from_all_groups -> add_to_group - подписчик всегда в актуальной группе
  • MailerLite webhook subscriber.unsubscribed -> note в Kommo
  • Один контакт = одна группа = одна nurturing-цепочка

Если нужна интеграция Kommo с MailerLite или другой email-платформой - опишите задачу команде Exceltic.dev.

Ещё статьи

Все →