Kommo + Reply.io: синхронизация аутрич-кампаний с воронкой продаж без дублей

Кастомная интеграция 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}. Логика проверки:

  1. Поиск контакта по email (query принимает строку и матчит по email, имени, телефону)
  2. Если найдено несколько контактов с тем же email - использовать первый созданный (наименьший id), логировать конфликт
  3. Если контакт найден, но в Kommo уже есть активная сделка в этой воронке - добавить только заметку, не создавать новую сделку
  4. Сделка создаётся только если нет активной сделки в целевой воронке

Аналогичная логика на стороне 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 оценит объём работ и предложит архитектуру под ваш стек.

Ещё статьи

Все →