Kommo + Customer.io: email-автоматизация на основе данных воронки

Customer.io - платформа email-автоматизации, ориентированная на data-driven сценарии: ветвление кампаний по атрибутам профиля, поведенческие триггеры, A/B тесты на уровне отправки. В B2B sales это означает возможность отправить разные последовательности писем исходя из индустрии лида, размера сделки или этапа воронки в Kommo. Без интеграции данные из CRM и данные в Customer.io существуют изолированно.

Типичная проблема: лид перешёл в этап «Коммерческое предложение» в Kommo - и должна автоматически запуститься nurture-последовательность в Customer.io. Или: клиент открыл три письма подряд - это сигнал интереса, и в Kommo должна появиться задача позвонить. Без двусторонней интеграции оба сценария требуют ручного вмешательства.

В этой статье разберём bidirectional-связку Kommo - Customer.io: события из воронки запускают email-кампании, а engagement-данные Customer.io возвращаются в карточку сделки.

Почему нативной интеграции нет

Customer.io не имеет готового виджета для Kommo. В Customer.io доступны прямые интеграции с Segment, Twilio, Salesforce и несколькими другими - Kommo в этот список не входит. Zapier поддерживает Customer.io, но только в одну сторону (Zapier -> Customer.io), и не передаёт полный набор атрибутов сделки, необходимый для data-driven ветвления.

Customer.io - платформа для отправки transactional и marketing email/push/SMS сообщений на основе событий и атрибутов пользователей. Отличается от Mailchimp/SendGrid акцентом на сегментацию по поведению и разветвлённые workflow.

Архитектура двусторонней интеграции

Направление 1 - Kommo -> Customer.io:

  • Kommo webhook (смена этапа / новый лид) -> обновление Customer.io профиля
  • Атрибуты сделки (размер, индустрия, этап) -> Customer.io person attributes
  • Смена этапа -> Customer.io custom event -> триггер Campaign

Направление 2 - Customer.io -> Kommo:

  • Customer.io webhook (email opened, clicked, unsubscribed) -> заметка в Kommo
  • Несколько opens подряд -> задача менеджеру позвонить

Реализация: Kommo -> Customer.io

Шаг 1 - подписка на Kommo webhook:

В Kommo настраиваем webhook на события: смена статуса сделки, добавление нового лида. В Kommo Admin -> Настройки -> Webhooks.

from flask import Flask, request
import requests, base64

app = Flask(__name__)

CIO_SITE_ID  = "your_site_id"     # из Customer.io Account -> API Credentials
CIO_API_KEY  = "your_api_key"
CIO_TRACK_BASE = "https://track.customer.io/api/v1"

KOMMO_DOMAIN = "yourdomain.kommo.com"
KOMMO_TOKEN  = "your_kommo_token"
KOMMO_BASE   = f"https://{KOMMO_DOMAIN}/api/v4"

def cio_headers():
    creds = base64.b64encode(f"{CIO_SITE_ID}:{CIO_API_KEY}".encode()).decode()
    return {
        "Authorization": f"Basic {creds}",
        "Content-Type":  "application/json"
    }

@app.route("/kommo/webhook", methods=["POST"])
def kommo_webhook():
    data = request.json
    # Kommo отправляет данные в формате {leads: {status_changed: [...]}}
    for event_type, leads in data.items():
        for entity_type, items in leads.items():
            for item in items:
                if entity_type == "status_changed":
                    on_lead_status_changed(item)
                elif entity_type == "add":
                    on_lead_added(item)
    return "ok", 200

Шаг 2 - передать данные Kommo в Customer.io:

def get_kommo_lead_full(lead_id: int) -> dict:
    hs = requests.Session()
    hs.headers.update({
        "Authorization": f"Bearer {KOMMO_TOKEN}",
        "Content-Type":  "application/json"
    })
    r = hs.get(f"{KOMMO_BASE}/leads/{lead_id}",
               params={"with": "contacts,custom_fields"})
    lead = r.json()

    # Находим email контакта
    contacts = lead.get("_embedded", {}).get("contacts", [])
    email = ""
    for c in contacts:
        for field in c.get("custom_fields_values") or []:
            if field.get("field_type") == "EMAIL":
                vals = field.get("values", [])
                if vals:
                    email = vals[0].get("value", "")
                    break

    return {"id": lead_id, "email": email, "lead": lead}

def on_lead_status_changed(item: dict):
    lead_id   = item.get("id")
    status_id = item.get("status_id")
    pipeline  = item.get("pipeline_id")

    ctx = get_kommo_lead_full(lead_id)
    if not ctx["email"]:
        return

    lead  = ctx["lead"]
    email = ctx["email"]

    # Карта этапов -> понятные названия
    stage_map = {
        1: "new", 2: "in_contact", 3: "proposal_sent",
        4: "negotiation", 142: "won", 143: "lost"
    }
    stage_name = stage_map.get(status_id, f"stage_{status_id}")

    # Обновить атрибуты в Customer.io
    cio = requests.Session()
    cio.headers.update(cio_headers())

    cio.put(f"{CIO_TRACK_BASE}/customers/{email}", json={
        "kommo_lead_id":    str(lead_id),
        "kommo_stage":      stage_name,
        "kommo_stage_id":   status_id,
        "kommo_pipeline":   pipeline,
        "deal_value":       lead.get("price", 0),
        "responsible_name": lead.get("responsible_user_id"),
    })

    # Отправить событие для триггера Campaign
    cio.post(f"{CIO_TRACK_BASE}/customers/{email}/events", json={
        "name": "kommo_stage_changed",
        "data": {
            "stage":      stage_name,
            "stage_id":   status_id,
            "lead_id":    lead_id,
            "deal_value": lead.get("price", 0),
        }
    })

def on_lead_added(item: dict):
    ctx = get_kommo_lead_full(item.get("id"))
    if not ctx["email"]:
        return

    lead  = ctx["lead"]
    email = ctx["email"]

    cio = requests.Session()
    cio.headers.update(cio_headers())

    # Идентифицировать нового пользователя в Customer.io
    cio.put(f"{CIO_TRACK_BASE}/customers/{email}", json={
        "kommo_lead_id": str(lead.get("id")),
        "kommo_stage":   "new",
        "deal_value":    lead.get("price", 0),
        "created_at":    lead.get("created_at"),
    })

    # Событие для welcome-sequence
    cio.post(f"{CIO_TRACK_BASE}/customers/{email}/events", json={
        "name": "kommo_lead_created",
        "data": {"lead_id": lead.get("id")}
    })

Реализация: Customer.io -> Kommo

Шаг 3 - обработка email-событий из Customer.io:

В Customer.io настраиваем Reporting Webhook: Workspace -> Settings -> Reporting Webhooks. Подписываемся на email_opened, email_clicked, email_unsubscribed.

@app.route("/customerio/webhook", methods=["POST"])
def cio_webhook():
    # Customer.io подписывает webhook через X-CIO-Signature (HMAC-SHA256)
    event_type  = request.json.get("metric", "")
    customer_id = request.json.get("customer_id", "")  # это email
    data        = request.json.get("data", {})

    if event_type in ("email_opened", "email_clicked"):
        on_email_engagement(customer_id, event_type, data)
    elif event_type == "email_unsubscribed":
        on_email_unsubscribed(customer_id, data)

    return "ok", 200

def find_kommo_lead_by_email(email: str) -> int | None:
    hs = requests.Session()
    hs.headers.update({
        "Authorization": f"Bearer {KOMMO_TOKEN}",
        "Content-Type":  "application/json"
    })
    r = hs.get(f"{KOMMO_BASE}/contacts", params={"query": email, "limit": 1})
    contacts = r.json().get("_embedded", {}).get("contacts", [])
    if not contacts:
        return None
    contact_id = contacts[0]["id"]
    r2 = hs.get(f"{KOMMO_BASE}/contacts/{contact_id}/links")
    links = r2.json().get("_embedded", {}).get("links", [])
    for l in links:
        if l.get("to_entity_type") == "leads":
            return l["to_entity_id"]
    return None

# Счётчик открытий для hot-lead триггера
_open_counts: dict = {}

def on_email_engagement(email: str, event_type: str, data: dict):
    lead_id = find_kommo_lead_by_email(email)
    if not lead_id:
        return

    event_label = {"email_opened": "открыл письмо", "email_clicked": "кликнул по ссылке"}
    subject     = data.get("subject", "")

    hs = requests.Session()
    hs.headers.update({
        "Authorization": f"Bearer {KOMMO_TOKEN}",
        "Content-Type":  "application/json"
    })
    hs.post(f"{KOMMO_BASE}/leads/notes", json=[{
        "entity_id": lead_id,
        "note_type":  "common",
        "params":     {"text": f"Customer.io: {email} {event_label[event_type]}. Тема: {subject}"}
    }])

    # Hot-lead логика: 3+ открытия = звонить
    key = f"{email}:opens"
    _open_counts[key] = _open_counts.get(key, 0) + 1

    if _open_counts[key] >= 3:
        import time
        hs.post(f"{KOMMO_BASE}/tasks", json=[{
            "task_type_id": 1,
            "entity_type":  "leads",
            "entity_id":    lead_id,
            "text":         f"{email} открыл письма 3 раза - высокий интерес. Позвоните сегодня.",
            "complete_till": int(time.time()) + 28800,  # 8 часов
        }])
        _open_counts[key] = 0  # сброс

В продакшене _open_counts замените на Redis-счётчик с TTL 48 часов - это предотвратит накопление устаревших данных.

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

B2B SaaS: 200 лидов в месяц через inbound, 4-недельный цикл продажи. До интеграции: Customer.io campains запускались вручную через ручной CSV-экспорт из Kommo раз в неделю. 20-30% лидов выпадали из nurture-последовательности из-за задержки синхронизации.

После двусторонней интеграции:

  • Каждый новый лид в Kommo автоматически идентифицируется в Customer.io
  • Смена этапа запускает нужную Campaign в течение секунды
  • «Горячие» лиды (3+ открытия) получают задачу менеджеру автоматически
  • 0 ручных экспортов CSV

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

B2B-команды, которые сочетают sales-воронку в Kommo с маркетинговой автоматизацией через Customer.io. Особенно эффективно для inbound-моделей, где лид проходит через email nurturing параллельно с работой менеджера.

Аналогичная интеграция для другой email-платформы описана в Kommo + Encharge. Если вы используете SendGrid для transactional email - там другая архитектура: смотрите Kommo + SendGrid.

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

Как ветвить Customer.io Campaign по размеру сделки из Kommo?

В Customer.io Campaign добавьте Segment Filter по атрибуту deal_value - он обновляется при каждой смене этапа. Campaign выходит только на персон, у которых deal_value >= 5000. Атрибуты передаются через Track API через PUT /customers/{email} - именно это мы делаем в on_lead_status_changed.

Нужен ли отдельный Customer.io тариф для webhook reporting?

Reporting Webhooks доступны на всех платных тарифах Customer.io: Essentials и выше. На Free тарифе webhooks не поддерживаются. Verify в Customer.io -> Settings -> Billing.

Как отписка в Customer.io отражается в Kommo?

В нашем примере email_unsubscribed создаёт заметку в Kommo и может изменить кастомное поле статуса подписки. Логику можно расширить: если лид отписался - автоматически ставить задачу менеджеру выяснить причину или переводить сделку на этап «Отказ».

Можно ли синхронизировать custom fields Kommo с Customer.io атрибутами?

Да. В get_kommo_lead_full разбираем custom_fields_values из Kommo и маппируем нужные поля в Customer.io атрибуты. Например, поле «Индустрия» из Kommo становится атрибутом industry в Customer.io для сегментации campains по вертикалям.

Итог

Kommo + Customer.io bidirectional-интеграция даёт полную картину: воронка в CRM управляет email-автоматизацией, а engagement-данные возвращаются в CRM. Схема:

  • Kommo webhook (статус, новый лид) -> Customer.io Track API: обновление атрибутов и событие-триггер
  • Customer.io Reporting Webhook -> Kommo: заметка об открытии, задача на горячего лида
  • Customer.io Campaign сегментируется по атрибутам сделки (deal_value, kommo_stage, industry)

Если ваша команда работает с Kommo и Customer.io - опишите задачу команде Exceltic.dev. Настроим двустороннюю синхронизацию под вашу структуру воронки и кампаний.

Ещё статьи

Все →