Кастомная интеграция Kommo и Reply.io работает следующим образом: webhook от Reply.io при событии email_replied или contact_called автоматически ищет контакт в Kommo по email и либо создаёт новую сделку, либо обновляет существующую. В обратном направлении - смена статуса сделки на «Квалифицирован» добавляет контакт в нужную последовательность через Reply.io API v3. Без единой строчки ручного переноса данных между инструментами.
Албато предлагает no-code коннектор между Kommo и Reply.io, но он не решает проблему дедупликации и не поддерживает двустороннюю синхронизацию с логикой на уровне этапов воронки. Для outbound-команд с объёмом 200+ проспектов в месяц это принципиальная разница.
В интеграционных проектах Exceltic.dev мы регулярно видим одну и ту же картину: SDR ведёт проспектинг в Reply.io, проспект отвечает - и это событие остаётся только в инбоксе Reply.io. Менеджер либо проверяет оба инструмента параллельно, либо вносит ответившего в Kommo вручную. Лиды теряются, дубли накапливаются, атрибуция ломается. Эта статья описывает архитектуру кастомной двусторонней интеграции, которая устраняет этот разрыв на уровне API.
Операционная боль outbound-команды
Компания использует Reply.io для outbound email-последовательностей и LinkedIn-аутрича. Воронка продаж живёт в Kommo. Когда проспект отвечает на письмо, менеджер видит ответ в инбоксе Reply.io - но Kommo об этом не знает. SDR вручную создаёт сделку в CRM, копирует текст ответа в заметку, присваивает ответственного. При объёме 50 проспектов в неделю это 3-4 часа ручной работы. При 200 - полноценная должность координатора.
Вторая проблема - дубли. Проспект может быть в Reply.io как Prospect и в Kommo как Lead, созданный с другого канала (конференция, входящий запрос). Без дедупликации по email команда работает с двумя несвязанными записями об одном человеке.
Третья - обратная синхронизация. Когда SDR квалифицирует лид в Kommo, следующий шаг - добавить контакт в соответствующую Reply.io последовательность для следующего касания. Без интеграции это снова ручная операция.
Почему Zapier и Make не решают задачу
Zapier предлагает триггер «New Reply in Reply.io» - он срабатывает при входящем сообщении в инбоксе. Проблема: Zapier не разграничивает типы событий (ответ на email, отписка, баунс, завершённый звонок) и не передаёт полный payload с контекстом последовательности. Создать сделку в Kommo можно, но без данных о том, из какой кампании пришёл проспект и какой шаг последовательности сработал.
Make (ex-Integromat) имеет тот же ограничение плюс проблему со стоимостью: при объёме 500 событий в месяц стоимость операций Make превышает стоимость разработки кастомного вебхук-обработчика на первый год использования.
Обратная синхронизация (Kommo -> Reply.io) в Zapier требует Webhook-триггер от Kommo при смене этапа, который настраивается через Digital Pipeline. Это уже требует backend - то есть no-code решение всё равно становится гибридным, но без контроля над логикой.
Главный архитектурный аргумент: Reply.io API v3 не подписывает webhook-запросы HMAC-подписью. Это означает, что без кастомного backend-слоя с проверкой shared secret в URL-параметре вебхук открыт для спуфинга. Zapier и Make не дают возможности добавить эту проверку на уровне получателя.
Архитектура двусторонней синхронизации
Кастомная интеграция состоит из трёх компонентов: webhook-обработчик (Python/FastAPI), слой дедупликации и конфигурационный маппинг кампаний к этапам воронки.
Reply.io --(webhook)--> [Backend] --(API)--> Kommo
Kommo --(webhook)--> [Backend] --(API)--> Reply.io
Backend запускается как отдельный сервис (Railway, Fly.io) и хранит только служебные данные: маппинг Reply.io sequence ID -> Kommo pipeline stage ID.
Reply.io -> Kommo: ответы проспектов в сделки
Reply.io доставляет webhook-события при изменении статуса проспекта в последовательности. Актуальные типы событий (по состоянию на Q2 2026, Reply.io API v3):
| Событие | Когда срабатывает |
|---|---|
email_replied | Проспект ответил на письмо последовательности |
email_bounced | Письмо вернулось с ошибкой доставки |
contact_opted_out | Проспект отписался |
contact_called | Завершён звонок по задаче из последовательности |
contact_finished | Проспект завершил последовательность |
Пример payload для email_replied:
{
"event": {
"type": "email_replied",
"date": "2026-06-10T14:22:00.000Z",
"user_id": 1234
},
"contact_fields": {
"id": 12345,
"email": "cto@prospect.com"
},
"sequence_fields": {
"id": 200,
"step_number": 3
},
"email_text": "<html>...</html>",
"reply_date": "2026-06-10T14:22:00.000Z"
}
Auth: Authorization: Bearer {REPLY_API_KEY} (Reply.io API v3 не использует x-api-key - это устаревший формат v1/v2).
Webhook-обработчик на Python:
from fastapi import FastAPI, Request, HTTPException
import httpx
app = FastAPI()
KOMMO_BASE = "https://{account}.kommo.com/api/v4"
KOMMO_TOKEN = "Bearer {kommo_long_lived_token}"
SHARED_SECRET = "{secret}" # передаётся в URL: /webhook?secret=...
SEQUENCE_TO_PIPELINE = {
200: {"pipeline_id": 1234567, "stage_id": 18766501}, # outbound_q1
205: {"pipeline_id": 1234567, "stage_id": 18766502}, # enterprise_seq
}
EVENT_NOTE_MAP = {
"email_replied": "Ответ на письмо (Reply.io)",
"contact_called": "Звонок завершён (Reply.io)",
"contact_finished": "Завершил последовательность (Reply.io)",
"email_bounced": "Письмо вернулось (Reply.io bounce)",
"contact_opted_out": "Отписался (Reply.io)",
}
@app.post("/reply-webhook")
async def reply_webhook(request: Request, secret: str):
if secret != SHARED_SECRET:
raise HTTPException(status_code=403, detail="Invalid secret")
data = await request.json()
event_type = data.get("event", {}).get("type")
email = data.get("contact_fields", {}).get("email")
sequence_id = data.get("sequence_fields", {}).get("id")
if not email or event_type not in EVENT_NOTE_MAP:
return {"status": "skipped"}
async with httpx.AsyncClient() as client:
# 1. Дедупликация: поиск контакта в Kommo по email
contact_id = await find_kommo_contact(client, email)
# 2. Если не найден - создать
if not contact_id:
contact_id = await create_kommo_contact(client, email, data)
# 3. Найти или создать сделку
lead_id = await find_or_create_lead(
client, contact_id, sequence_id, event_type
)
# 4. Добавить заметку с контекстом
note_text = EVENT_NOTE_MAP[event_type]
if event_type == "email_replied":
reply_snippet = (data.get("email_text") or "")[:500]
note_text = f"{note_text}:\n{reply_snippet}"
elif event_type == "contact_called":
note_text = (
f"{note_text}: {data.get('disposition', '')}, "
f"{data.get('duration', 0)} сек. {data.get('notes', '')}"
)
await add_kommo_note(client, lead_id, note_text)
return {"status": "ok", "lead_id": lead_id}
async def find_kommo_contact(client, email: str):
r = await client.get(
f"{KOMMO_BASE}/contacts",
params={"query": email},
headers={"Authorization": KOMMO_TOKEN},
)
if r.status_code == 200:
items = r.json().get("_embedded", {}).get("contacts", [])
return items[0]["id"] if items else None
return None
async def create_kommo_contact(client, email: str, data: dict):
payload = [
{
"name": email,
"custom_fields_values": [
{"field_code": "EMAIL", "values": [{"value": email, "enum_code": "WORK"}]}
],
}
]
r = await client.post(
f"{KOMMO_BASE}/contacts",
json=payload,
headers={"Authorization": KOMMO_TOKEN},
)
items = r.json().get("_embedded", {}).get("contacts", [])
return items[0]["id"] if items else None
async def find_or_create_lead(client, contact_id: int, sequence_id: int, event_type: str):
# Поиск активной сделки для этого контакта в нужной воронке
mapping = SEQUENCE_TO_PIPELINE.get(sequence_id, {})
pipeline_id = mapping.get("pipeline_id")
r = await client.get(
f"{KOMMO_BASE}/leads",
params={"filter[contact_id]": contact_id, "filter[pipeline_id]": pipeline_id},
headers={"Authorization": KOMMO_TOKEN},
)
items = r.json().get("_embedded", {}).get("leads", []) if r.status_code == 200 else []
if items:
return items[0]["id"]
# Создать новую сделку
stage_id = mapping.get("stage_id")
payload = [
{
"name": f"Reply.io outbound ({event_type})",
"pipeline_id": pipeline_id,
"status_id": stage_id,
"_embedded": {"contacts": [{"id": contact_id}]},
}
]
r = await client.post(
f"{KOMMO_BASE}/leads",
json=payload,
headers={"Authorization": KOMMO_TOKEN},
)
items = r.json().get("_embedded", {}).get("leads", [])
return items[0]["id"] if items else None
async def add_kommo_note(client, lead_id: int, text: str):
payload = [
{
"entity_id": lead_id,
"note_type": "common",
"params": {"text": text},
}
]
await client.post(
f"{KOMMO_BASE}/leads/notes",
json=payload,
headers={"Authorization": KOMMO_TOKEN},
)
Важный момент: Reply.io не подписывает webhook HMAC-подписью (в отличие от Stripe или Kommo). Верификация строится на shared secret в URL-параметре: POST /reply-webhook?secret=random_64char_string. Этот параметр задаётся при регистрации webhook-подписки через Reply.io API и хранится в переменных среды backend.
Kommo -> Reply.io: добавление в кампанию при квалификации
Kommo отправляет webhook при смене этапа сделки через Digital Pipeline или через стандартные вебхуки в настройках аккаунта. При переходе сделки в этап «Квалифицирован» backend получает событие и добавляет контакт в соответствующую Reply.io последовательность.
KOMMO_TO_REPLY_SEQUENCE = {
18766503: 200, # Kommo stage "Qualified" -> Reply.io sequence 200
18766504: 205, # Kommo stage "Enterprise" -> Reply.io sequence 205
}
REPLY_BASE = "https://api.reply.io/v3"
REPLY_TOKEN = "Bearer {reply_api_key}"
@app.post("/kommo-webhook")
async def kommo_webhook(request: Request):
data = await request.json()
# Kommo отправляет данные в поле leads[status]
lead_events = data.get("leads", {}).get("status", [])
for event in lead_events:
stage_id = int(event.get("status_id", 0))
if stage_id not in KOMMO_TO_REPLY_SEQUENCE:
continue
lead_id = event.get("id")
sequence_id = KOMMO_TO_REPLY_SEQUENCE[stage_id]
async with httpx.AsyncClient() as client:
# Получить email контакта из сделки
lead = await get_kommo_lead(client, lead_id)
email = extract_email_from_lead(lead)
if not email:
continue
# Найти или создать контакт в Reply.io
reply_contact_id = await find_reply_contact(client, email)
if not reply_contact_id:
reply_contact_id = await create_reply_contact(client, lead)
# Добавить в последовательность
await add_to_reply_sequence(client, sequence_id, reply_contact_id)
return {"status": "ok"}
async def find_reply_contact(client, email: str):
r = await client.get(
f"{REPLY_BASE}/contacts",
params={"filter[email]": email},
headers={"Authorization": REPLY_TOKEN},
)
if r.status_code == 200:
items = r.json().get("items", [])
return items[0]["id"] if items else None
return None
async def create_reply_contact(client, lead: dict):
contact_data = extract_contact_fields(lead) # имя, email из кастомных полей
r = await client.post(
f"{REPLY_BASE}/contacts",
json=contact_data,
headers={"Authorization": REPLY_TOKEN},
)
return r.json().get("id")
async def add_to_reply_sequence(client, sequence_id: int, contact_id: int):
payload = {"contactId": contact_id}
await client.post(
f"{REPLY_BASE}/sequences/{sequence_id}/contacts",
json=payload,
headers={"Authorization": REPLY_TOKEN},
)
Endpoint для добавления контакта в последовательность: POST /v3/sequences/{sequence_id}/contacts с телом {"contactId": <id>}. Rate limit Reply.io API v3: 100 запросов в минуту, 3000 в час. При пакетной обработке используйте exponential backoff при получении статуса 429.
Дедупликация контактов
Дедупликация контактов - это проверка существования записи перед её созданием, чтобы исключить появление дублей в CRM.
Kommo API предоставляет поиск по email через эндпоинт GET /api/v4/contacts?query={email}. Логика проверки:
- Поиск контакта по email (
queryпринимает строку и матчит по email, имени, телефону) - Если найдено несколько контактов с тем же email - использовать первый созданный (наименьший
id), логировать конфликт - Если контакт найден, но в Kommo уже есть активная сделка в этой воронке - добавить только заметку, не создавать новую сделку
- Сделка создаётся только если нет активной сделки в целевой воронке
Аналогичная логика на стороне Reply.io: перед добавлением контакта в последовательность - проверка через GET /v3/contacts?filter[email]={email}. Если контакт уже является участником этой последовательности - пропустить (Reply.io возвращает ошибку 409 при попытке добавить в одну последовательность дважды, её нужно перехватывать).
Для аналогичной синхронизации лидов из других outbound-инструментов - посмотрите статью про интеграцию Kommo и Apollo.io: схема дедупликации там идентична, отличается только источник и структура payload.
Реальный кейс
B2B SaaS-компания, 12 человек, outbound-команда из 2 SDR. Reply.io: 4 активные последовательности, 300-400 проспектов в месяц. Kommo: воронка продаж, 2 AE.
До интеграции: SDR проверял Reply.io инбокс и вручную переносил ответивших проспектов в Kommo. Среднее время переноса - 2-4 часа после ответа. Часть ответов терялась при переносе (SDR забывал скопировать текст ответа в заметку). Квалифицированных лидов добавляли в Reply.io вручную раз в неделю на «планёрке».
После интеграции:
- Ответ проспекта -> сделка в Kommo за 3-5 секунд
- Текст ответа автоматически попадает в заметку к сделке
- При квалификации AE в Kommo контакт немедленно добавляется в follow-up последовательность Reply.io
- Bounce и отписки из Reply.io создают сделку с отдельным статусом (не засоряют воронку активных лидов)
Цифры за первые 60 дней: 0 пропущенных ответов (ранее SDR оценивал потери в 10-15% ответов), время реакции менеджера сократилось с 4 часов до 20 минут. Экономия ручного труда SDR - около 6 часов в неделю.
Для понимания общей картины по эффективности аутрич-каналов - такие данные удобно смотреть в Prooflytics: платформа замыкает UTM-атрибуцию из Reply.io на сделки в Kommo и показывает реальный cost per deal по каждой кампании.
Для кого подходит
Интеграция Kommo + Reply.io актуальна для:
- Outbound-первые B2B компании (15-100 человек): Reply.io - основной канал первого касания, Kommo - для ведения тёплых лидов
- SDR/AE модель: SDR проспектирует в Reply.io, AE работает в Kommo - нужен чёткий handoff между инструментами
- Команды с объёмом 200+ проспектов в месяц: при меньшем объёме ручной перенос ещё терпим, при большем - неизбежные потери
- Компании, которые уже попробовали Albato или Zapier и столкнулись с дублями или потерей данных об ответах
Интеграция не нужна, если Reply.io используется только для LinkedIn-аутрича без email (объём вебхуков низкий, ручной перенос справляется) или если воронка продаж линейная и SDR сам закрывает сделки без передачи AE.
Подробнее о настройке самой воронки в Kommo для outbound-сценариев - в статье о настройке воронки Kommo CRM.
Для общего понимания возможностей кастомных интеграций с Kommo - читайте обзор кастомных интеграций Kommo CRM.
Часто задаваемые вопросы
Можно ли подключить Reply.io к Kommo без программирования?
Албато предлагает no-code коннектор, который закрывает базовый сценарий: событие в Reply.io -> действие в Kommo. Но он не поддерживает дедупликацию контактов (дубли неизбежны при повторных кампаниях), не умеет различать типы событий для маршрутизации в разные воронки и не реализует обратную синхронизацию (Kommo -> Reply.io при квалификации). Для команд с объёмом больше 100 проспектов в месяц Albato создаёт больше проблем, чем решает.
Reply.io подписывает webhook HMAC-подписью?
Нет. По состоянию на Q2 2026, Reply.io API v3 не добавляет HMAC-подпись к webhook-запросам. Верификация реализуется через shared secret в URL-параметре, который задаётся при создании webhook-подписки и хранится в переменных среды backend. Это менее надёжно, чем HMAC, но достаточно при использовании HTTPS и ротации секрета раз в квартал.
Что происходит если один и тот же проспект есть и в Reply.io, и в Kommo?
Это типичный случай при смешанном inbound/outbound стеке. Интеграция проверяет Kommo по email перед созданием контакта (GET /api/v4/contacts?query={email}). Если контакт найден - сделка добавляется к существующему контакту, а не создаётся новый. Если в Kommo уже есть активная сделка по этому контакту в целевой воронке - добавляется только заметка с текстом ответа.
Как обрабатываются bounce и отписки из Reply.io?
События email_bounced и contact_opted_out создают сделку с отдельным статусом в Kommo (например, «Недоступен» или «Отписался»). Менеджер видит этих проспектов отдельно от активного pipeline. Bounce с типом Hard дополнительно помечается тегом в Kommo для последующей чистки базы. Добавлять bounce-контакты в основную воронку не нужно - это засоряет CRM нерелевантными записями.
Сколько занимает разработка такой интеграции?
Типовой проект занимает 2-3 недели: 1 неделя на разработку и тестирование webhook-обработчика, 1 неделя на настройку маппинга воронок и тестирование на реальных данных, несколько дней на деплой и мониторинг. Стоимость зависит от количества воронок в Kommo и количества последовательностей в Reply.io, которые нужно связать.
Итого
- Reply.io и Kommo не имеют нативной интеграции; Albato закрывает только базовый односторонний поток без дедупликации
- Кастомная интеграция строится на webhook-событиях Reply.io (
email_replied,contact_called,email_bounced,contact_opted_out,contact_finished) и Kommo API v4 для поиска, создания контактов и сделок - Дедупликация обязательна: поиск по email перед созданием контакта, проверка активных сделок перед созданием новой
- Reply.io API v3 использует
Authorization: Bearer {api_key}и не подписывает webhooks - верификация через shared secret в URL - Обратная синхронизация: смена этапа в Kommo ->
POST /v3/sequences/{id}/contactsв Reply.io - Типовой объём работы: 2-3 недели разработки
Если ваша outbound-команда работает в Reply.io, а сделки ведёт в Kommo - опишите текущую схему: сколько проспектов в месяц, сколько воронок, как сейчас выглядит handoff между SDR и AE. Exceltic.dev оценит объём работ и предложит архитектуру под ваш стек.