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

Ortto (до ребрендинга - Autopilot) - Customer Data Platform (CDP) с встроенной email-автоматизацией, SMS, in-app сообщениями и аналитикой пути клиента. В отличие от обычных email-инструментов, Ortto строит единый профиль пользователя из всех каналов (web, email, CRM, продукт) и запускает автоматизации на основе поведения. Для Kommo интеграция с Ortto позволяет передавать данные о стадии воронки в CDP и запускать персонализированные email-последовательности в зависимости от прогресса сделки.

Ortto API работает через заголовок X-Api-Key. Ключевые операции: upsert person (создать или обновить профиль), track activity (зафиксировать событие), trigger journey (запустить автоматизацию). Bidirectional: Ortto может слать webhook при определённых событиях (email opened, link clicked, unsubscribed).

Ortto Journey - визуальный конструктор автоматизаций: при наступлении события (activity) запустить последовательность email/SMS/задач. Аналог Sequences в HubSpot или Automation в ActiveCampaign.

Почему Ortto, а не Mailchimp или ActiveCampaign

Ortto строит unified person profile из нескольких источников. Если контакт пришёл из Kommo как лид, потом стал пользователем продукта, потом написал в поддержку - всё это один профиль в Ortto. ActiveCampaign и Mailchimp работают как изолированные email-инструменты без CDP-слоя.

Для B2B SaaS с длинным циклом продаж это означает: email-кампании учитывают не только стадию в Kommo, но и то, как пользователь взаимодействует с продуктом (логины, features used, depth of usage).

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

Kommo: сделка переходит в новый этап
  -> Kommo webhook -> Ваш сервер
  -> Ortto API: upsert person (email, name, kommo_stage)
  -> Ortto API: track activity "crm_stage_changed"

Ortto Journey: триггер "crm_stage_changed" где stage = "Квалификация"
  -> Email 1: "Как прошёл первый звонок?"
  -> Wait 2 дня
  -> Email 2: Кейс-стади релевантный сегменту
  -> Wait 3 дня -> Check: открыл ли email?
  -> Ветка: открыл -> задача менеджеру в Kommo

Ortto webhook: email opened, link clicked
  -> Ваш сервер
  -> Kommo: обновить поле "Email активность"

Реализация: upsert person + track activity

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

app = Flask(__name__)

ORTTO_API_KEY    = os.environ["ORTTO_API_KEY"]
ORTTO_REGION     = os.environ.get("ORTTO_REGION", "api")  # api (US) или api.eu (EU)

KOMMO_SUBDOMAIN  = os.environ["KOMMO_SUBDOMAIN"]
KOMMO_TOKEN      = os.environ["KOMMO_ACCESS_TOKEN"]
KOMMO_STAGE_MAP  = {
    # stage_id -> (ortto_stage_label, journey_activity)
    1234: ("Новый лид",       "crm_stage_new_lead"),
    1235: ("Квалификация",    "crm_stage_qualification"),
    1236: ("КП отправлено",   "crm_stage_proposal_sent"),
    1237: ("Переговоры",      "crm_stage_negotiation"),
    1238: ("Закрыта",         "crm_stage_closed_won"),
}

ORTTO_BASE = f"https://{ORTTO_REGION}.ap3api.com"
ORTTO_HDR  = {"X-Api-Key": ORTTO_API_KEY, "Content-Type": "application/json"}
KOMMO_BASE = f"https://{KOMMO_SUBDOMAIN}.kommo.com/api/v4"
KOMMO_HDR  = {"Authorization": f"Bearer {KOMMO_TOKEN}", "Content-Type": "application/json"}

def get_lead_contact(lead_id: int) -> tuple[dict, dict]:
    r = requests.get(
        f"{KOMMO_BASE}/leads/{lead_id}",
        headers=KOMMO_HDR,
        params={"with": "contacts,custom_fields_values"},
    )
    lead = r.json()
    contacts = lead.get("_embedded", {}).get("contacts", [])
    contact = {}
    if contacts:
        rc = requests.get(
            f"{KOMMO_BASE}/contacts/{contacts[0]['id']}",
            headers=KOMMO_HDR,
            params={"with": "custom_fields_values"},
        )
        contact = rc.json()
    return lead, contact

def extract_email_and_name(contact: dict) -> tuple[str, str, str]:
    email = ""
    for cf in contact.get("custom_fields_values", []) or []:
        if cf.get("field_code") == "EMAIL":
            vals = cf.get("values", [])
            if vals:
                email = vals[0].get("value", "")
                break
    full_name = contact.get("name", "")
    parts     = full_name.split(" ", 1)
    first     = parts[0] if parts else ""
    last      = parts[1] if len(parts) > 1 else ""
    return email, first, last

def ortto_upsert_person(email: str, first: str, last: str, kommo_lead_id: int, stage: str):
    payload = {
        "people": [{
            "fields": {
                "str::email":          {"v": email},
                "str::first":          {"v": first},
                "str::last":           {"v": last},
                "str::kommo_lead_id":  {"v": str(kommo_lead_id)},
                "str::kommo_stage":    {"v": stage},
            }
        }],
        "merge_by":     ["str::email"],
        "merge_strategy": 2,  # MERGE: обновить существующий профиль
    }
    r = requests.post(f"{ORTTO_BASE}/v1/persons/merge", headers=ORTTO_HDR, json=payload)
    r.raise_for_status()
    return r.json().get("people", [{}])[0].get("id", "")

def ortto_track_activity(person_id: str, activity_id: str, attributes: dict):
    payload = {
        "activities": [{
            "activity_id": activity_id,  # "act:cm:crm-stage-changed" (настраивается в Ortto)
            "person_id":   person_id,
            "fields":      {k: {"v": v} for k, v in attributes.items()},
        }]
    }
    requests.post(f"{ORTTO_BASE}/v1/activities/create", headers=ORTTO_HDR, json=payload)

@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")

        stage_info = KOMMO_STAGE_MAP.get(new_status)
        if not stage_info:
            continue

        stage_label, activity_id = stage_info
        lead, contact            = get_lead_contact(lead_id)
        email, first, last       = extract_email_and_name(contact)

        if not email:
            continue

        person_id = ortto_upsert_person(email, first, last, lead_id, stage_label)

        ortto_track_activity(person_id, activity_id, {
            "str::lead_id":    str(lead_id),
            "str::stage":      stage_label,
            "dbl::deal_value": str(lead.get("price", 0) or 0),
        })

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

Обратная связь: Ortto webhook -> Kommo

Ortto отправляет webhook при: email opened, link clicked, unsubscribed, journey completed.

KOMMO_CF_EMAIL_ACTIVITY = int(os.environ["KOMMO_CF_EMAIL_ACTIVITY"])

@app.route("/webhooks/ortto", methods=["POST"])
def ortto_webhook():
    event    = request.json or {}
    ev_type  = event.get("type", "")

    if ev_type not in ("email.opened", "email.link_clicked"):
        return jsonify({"status": "ignored"}), 200

    person   = event.get("person", {})
    fields   = person.get("fields", {})
    lead_id  = fields.get("str::kommo_lead_id", {}).get("v", "")
    email_subject = event.get("email", {}).get("subject", "")

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

    action_label = "открыл email" if ev_type == "email.opened" else "кликнул в email"
    note_text    = f"Ortto: контакт {action_label}: '{email_subject}'"

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

    # Обновить custom field "Последняя email-активность"
    requests.patch(
        f"{KOMMO_BASE}/leads/{lead_id}",
        headers=KOMMO_HDR,
        json={"custom_fields_values": [{
            "field_id": KOMMO_CF_EMAIL_ACTIVITY,
            "values":   [{"value": f"{ev_type}: {email_subject}"}],
        }]},
    )

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

Настройка кастомных полей в Ortto

Для хранения данных из Kommo нужно добавить кастомные поля в Ortto:

  1. Ortto -> Settings -> Data -> Person Fields -> Add Field

    • kommo_lead_id (Text): ID сделки в Kommo
    • kommo_stage (Text): текущий этап воронки
  2. Ortto -> Activities -> Add Activity

    • crm_stage_changed: событие смены этапа
    • Поля активности: lead_id (Text), stage (Text), deal_value (Number)
  3. Ortto -> Journeys -> New Journey

    • Trigger: Activity crm_stage_changed where stage = “Квалификация”
    • Step 1: Send email “Результаты первого звонка”
    • Wait: 2 дня, unless email opened (Ранний выход)
    • Step 2: Send case study email

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

B2B SaaS, 150 лидов в месяц. До Ortto: массовые email-рассылки без персонализации по этапу воронки. Conversion rate email -> встреча: 2.1%. После интеграции Kommo + Ortto: email-последовательности запускаются автоматически при смене стадии, subject lines персонализированы по сегменту. Conversion rate: 4.8%. Дополнительных лидов: +23 встречи в месяц.

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

B2B SaaS с длинным циклом продаж (30+ дней) и командой 10-50 человек. Особенно если маркетинг и продажи используют разные инструменты и между ними разрыв - Ortto как CDP его закрывает. Ortto дороже Mailchimp ($99+/мес), но дешевле Marketo или Eloqua.

Аналогичный подход к email-автоматизации описан для Kommo + Customer.io и Kommo + Campaign Monitor.

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

Как работает merge_strategy в Ortto person upsert?

merge_strategy: 1 (OVERWRITE) - перезапишет все поля существующего профиля. merge_strategy: 2 (MERGE) - обновит только переданные поля, сохранит остальные. Для CRM-интеграции всегда используйте MERGE: не нужно передавать все поля из Kommo при каждом обновлении.

Ortto хранит GDPR-данные в ЕС?

Да, при использовании EU region (api.eu.ap3api.com). При инициализации аккаунта выбрать EU Data Center. Ранее созданные аккаунты в US регионе данные не перемещают. Если GDPR критичен - убедитесь что Ortto аккаунт создан в EU.

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

Webhook person.unsubscribed содержит person.fields с kommo_lead_id. Обновите custom field “Email статус” в Kommo значением “Отписался”. Это предотвратит ручную отправку email через Kommo человеку, который отписался от Ortto-рассылок.

Итог

Kommo + Ortto - CDP-слой для B2B email-автоматизации:

  • X-Api-Key header, EU region для GDPR
  • POST /v1/persons/merge с merge_strategy: 2 при смене этапа Kommo
  • POST /v1/activities/create с activity_id - триггер для Journey
  • Кастомное поле kommo_lead_id для обратной корреляции
  • Ortto webhook email.opened -> note в Kommo + update custom field

Если нужна CDP-интеграция Kommo с Ortto - опишите вашу воронку команде Exceltic.dev.

Ещё статьи

Все →