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:
-
Ortto -> Settings -> Data -> Person Fields -> Add Field
kommo_lead_id(Text): ID сделки в Kommokommo_stage(Text): текущий этап воронки
-
Ortto -> Activities -> Add Activity
crm_stage_changed: событие смены этапа- Поля активности:
lead_id(Text),stage(Text),deal_value(Number)
-
Ortto -> Journeys -> New Journey
- Trigger: Activity
crm_stage_changedwhere stage = “Квалификация” - Step 1: Send email “Результаты первого звонка”
- Wait: 2 дня, unless email opened (Ранний выход)
- Step 2: Send case study email
- Trigger: Activity
Реальный кейс
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-Keyheader, EU region для GDPRPOST /v1/persons/mergeсmerge_strategy: 2при смене этапа KommoPOST /v1/activities/createсactivity_id- триггер для Journey- Кастомное поле
kommo_lead_idдля обратной корреляции - Ortto webhook
email.opened-> note в Kommo + update custom field
Если нужна CDP-интеграция Kommo с Ortto - опишите вашу воронку команде Exceltic.dev.