Kommo + Grain: AI-саммари встреч и транскрипты в карточку сделки

Grain записывает встречу в Zoom или Google Meet, расшифровывает её и генерирует AI-саммари. Если подключить Grain к Kommo через webhook, транскрипт и ключевые тезисы появятся в карточке нужной сделки автоматически - без ручного копирования.

В этой статье разберём, как именно это работает на уровне API: какой webhook слушать, как сопоставить встречу со сделкой по email участника и как записать результат в Kommo через стандартный API заметок.

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

Grain поддерживает прямые интеграции с HubSpot, Salesforce и Notion. Kommo в этом списке нет. Это не случайность - Grain ориентируется на западный enterprise-рынок, где Kommo представлен слабее.

Обходные пути через Zapier технически работают, но имеют ограничения: Zapier триггерит на recording_added, но не даёт полного контроля над тем, что именно попадает в заметку. Форматирование теряется, длинный транскрипт обрезается на лимите символов Zapier-поля, а AI-саммари и обычный транскрипт нужно передавать отдельными шагами. Кастомная интеграция решает всё это нативно.

Архитектура - что реализовали

Схема простая: Grain отправляет webhook при готовности записи, сервис-посредник находит нужную сделку в Kommo по email участника и записывает в неё заметку с транскриптом и саммари.

Никакой дополнительной БД не нужно - маппинг происходит через поиск по контактам Kommo в момент получения события.

Настройка Grain webhook

Grain поддерживает два типа событий, которые нас интересуют:

  • recording_added - запись появилась в системе (транскрипт может ещё обрабатываться)
  • recording_updated - запись обновилась (транскрипт готов, AI-саммари готово)

Для нашей задачи подходит recording_updated - к этому моменту и транскрипт, и саммари уже доступны через API.

Создаём webhook через Grain API:

import requests

GRAIN_API_KEY = "your_grain_api_key"

response = requests.post(
    "https://grain.com/_/public-api/v2/hooks/create",
    headers={
        "Authorization": f"Bearer {GRAIN_API_KEY}",
        "Content-Type": "application/json",
    },
    json={
        "hook_type": "recording_updated",
        "hook_url": "https://your-server.com/grain-webhook",
        "include": {
            "participants": True,
            "ai_summary": True
        }
    }
)
print(response.json())

Параметр include критически важен: без него payload не будет содержать ни участников, ни AI-саммари - только базовые метаданные записи.

Пример payload, который придёт на ваш endpoint:

{
  "type": "recording_updated",
  "user_id": "eeee1111-ff22-gg33-hh44-iiii55555555",
  "data": {
    "id": "pppp6666-qq77-rr88-ss99-tttt00000000",
    "title": "Demo call - Acme Corp",
    "source": "zoom",
    "url": "https://grain.com/share/recording/pppp6666-qq77-rr88-ss99-tttt00000000",
    "start_datetime": "2025-06-15T14:00:00Z",
    "end_datetime": "2025-06-15T14:45:00Z",
    "duration_ms": 2700000,
    "participants": [
      {
        "id": "aaaa1111-bb22-cc33-dd44-eeee55555555",
        "name": "Ivan Petrov",
        "email": "ivan@acmecorp.com",
        "scope": "external",
        "confirmed_attendee": true
      },
      {
        "id": "bbbb2222-cc33-dd44-ee55-ffff66666666",
        "name": "Anna Sales",
        "email": "anna@yourcompany.com",
        "scope": "internal",
        "confirmed_attendee": true
      }
    ],
    "ai_summary": "Клиент заинтересован в интеграции с ERP-системой. Основное препятствие - согласование бюджета с финансовым директором до конца квартала. Договорились о следующем звонке через 2 недели."
  }
}

Транскрипт в payload не включается - он доступен отдельным запросом к API.

Маппинг встречи на сделку в Kommo

Самый надёжный способ найти нужную сделку - пройтись по email внешних участников встречи и найти соответствующие контакты в Kommo, а затем получить привязанные к ним сделки.

import requests
from typing import Optional

KOMMO_DOMAIN = "yourcompany.kommo.com"
KOMMO_TOKEN = "your_kommo_long_lived_token"

def find_lead_by_participant_emails(participant_emails: list[str]) -> Optional[dict]:
    """
    Ищет активную сделку в Kommo по email внешнего участника встречи.
    Возвращает первую найденную сделку или None.
    """
    for email in participant_emails:
        # Ищем контакт по email
        resp = requests.get(
            f"https://{KOMMO_DOMAIN}/api/v4/contacts",
            headers={"Authorization": f"Bearer {KOMMO_TOKEN}"},
            params={"query": email, "with": "leads"}
        )
        data = resp.json()
        contacts = data.get("_embedded", {}).get("contacts", [])
        
        for contact in contacts:
            leads = contact.get("_embedded", {}).get("leads", [])
            if leads:
                # Берём самую последнюю сделку
                lead_id = leads[-1]["id"]
                lead_resp = requests.get(
                    f"https://{KOMMO_DOMAIN}/api/v4/leads/{lead_id}",
                    headers={"Authorization": f"Bearer {KOMMO_TOKEN}"}
                )
                return lead_resp.json()
    
    return None

Если у клиента несколько активных сделок, функция вернёт последнюю по ID. При необходимости логику можно усложнить: например, фильтровать только сделки в определённых статусах или на определённых этапах воронки.

Добавление транскрипта и саммари в карточку

Получаем транскрипт через Grain API и формируем заметку:

def get_grain_transcript(recording_id: str) -> str:
    """
    Получает транскрипт записи в текстовом формате.
    """
    resp = requests.get(
        f"https://grain.com/_/public-api/v2/recordings/{recording_id}/transcript.txt",
        headers={"Authorization": f"Bearer {GRAIN_API_KEY}"}
    )
    return resp.text if resp.status_code == 200 else ""


def post_note_to_lead(lead_id: int, recording_data: dict, transcript: str) -> bool:
    """
    Записывает заметку с саммари и транскриптом в карточку сделки Kommo.
    """
    duration_min = recording_data.get("duration_ms", 0) // 60000
    title = recording_data.get("title", "Встреча")
    grain_url = recording_data.get("url", "")
    ai_summary = recording_data.get("ai_summary", "")
    
    # Формируем текст заметки
    note_text_parts = [
        f"Запись встречи: {title} ({duration_min} мин.)",
        f"Ссылка на запись в Grain: {grain_url}",
    ]
    
    if ai_summary:
        note_text_parts.append(f"\nAI-саммари:\n{ai_summary}")
    
    if transcript:
        # Обрезаем транскрипт до разумного размера для заметки
        transcript_preview = transcript[:3000]
        if len(transcript) > 3000:
            transcript_preview += "\n\n[транскрипт обрезан, полная версия - по ссылке выше]"
        note_text_parts.append(f"\nТранскрипт:\n{transcript_preview}")
    
    note_text = "\n".join(note_text_parts)
    
    resp = requests.post(
        f"https://{KOMMO_DOMAIN}/api/v4/leads/{lead_id}/notes",
        headers={
            "Authorization": f"Bearer {KOMMO_TOKEN}",
            "Content-Type": "application/json",
        },
        json=[{
            "note_type": "common",
            "params": {
                "text": note_text
            }
        }]
    )
    return resp.status_code == 200

Отдельно сохраняем ссылку на запись в кастомное поле сделки:

def save_grain_url_to_custom_field(
    lead_id: int,
    grain_url: str,
    custom_field_id: int
) -> bool:
    """
    Сохраняет ссылку на запись Grain в кастомное поле сделки.
    custom_field_id - ID поля "Запись встречи" в Kommo.
    """
    resp = requests.patch(
        f"https://{KOMMO_DOMAIN}/api/v4/leads/{lead_id}",
        headers={
            "Authorization": f"Bearer {KOMMO_TOKEN}",
            "Content-Type": "application/json",
        },
        json={
            "custom_fields_values": [{
                "field_id": custom_field_id,
                "values": [{"value": grain_url}]
            }]
        }
    )
    return resp.status_code == 200

Кастомное поле типа URL нужно создать заранее в настройках Kommo - через раздел «Настройки -> Поля сделок». ID поля можно получить через GET /api/v4/leads/custom_fields.

Обработка краевых случаев

Главный webhook-обработчик собирает всё вместе:

from flask import Flask, request, jsonify
import logging

app = Flask(__name__)
logger = logging.getLogger(__name__)

GRAIN_URL_FIELD_ID = 123456  # ID кастомного поля в Kommo

@app.route("/grain-webhook", methods=["POST"])
def handle_grain_webhook():
    payload = request.json
    event_type = payload.get("type")
    
    if event_type != "recording_updated":
        return jsonify({"status": "skipped"}), 200
    
    recording = payload.get("data", {})
    recording_id = recording.get("id")
    participants = recording.get("participants", [])
    
    # Берём только внешних участников
    external_emails = [
        p["email"] for p in participants
        if p.get("scope") == "external" and p.get("email")
    ]
    
    if not external_emails:
        logger.warning(f"Grain recording {recording_id}: no external participants found")
        return jsonify({"status": "no_external_participants"}), 200
    
    lead = find_lead_by_participant_emails(external_emails)
    
    if not lead:
        # Сделка не найдена - логируем для ручной обработки
        logger.warning(
            f"Grain recording {recording_id}: no matching lead found "
            f"for emails: {external_emails}"
        )
        # Можно отправить уведомление в Slack или на email
        return jsonify({"status": "lead_not_found"}), 200
    
    lead_id = lead["id"]
    transcript = get_grain_transcript(recording_id)
    
    # Записываем заметку
    note_ok = post_note_to_lead(lead_id, recording, transcript)
    
    # Сохраняем ссылку в кастомное поле
    url_ok = save_grain_url_to_custom_field(
        lead_id,
        recording.get("url", ""),
        GRAIN_URL_FIELD_ID
    )
    
    logger.info(
        f"Grain recording {recording_id} -> lead {lead_id}: "
        f"note={note_ok}, url_field={url_ok}"
    )
    
    return jsonify({
        "status": "ok",
        "lead_id": lead_id,
        "note_created": note_ok
    }), 200

Отдельно стоит упомянуть ещё два краевых случая:

Внутренние встречи без клиента. Если все участники встречи - сотрудники вашей компании, external_emails будет пустым. Функция вернёт no_external_participants и запись в CRM не создастся. Это правильное поведение - во внутренние встречи писать не нужно.

Повторные срабатывания. Grain может отправить recording_updated несколько раз, пока AI-обработка завершается поэтапно. Чтобы не дублировать заметки, проверяйте наличие заметки с данным recording_id перед созданием новой - либо храните обработанные ID в Redis с TTL 24 часа.

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

К нам обратился head of sales SaaS-компании с командой из 8 AE. Проблема была стандартная: после демо-звонка каждый менеджер тратил 15-20 минут на то, чтобы перенести ключевые тезисы из Grain в карточку сделки в Kommo. Часть этого делалась вручную, часть - не делалась вовсе.

Мы настроили интеграцию за 3 дня. Схема оказалась чуть сложнее описанной выше: в компании одновременно могло быть несколько активных сделок с одним клиентом, поэтому маппинг шёл не просто по email, а по комбинации email + активная стадия воронки.

Через две недели после запуска:

  • Время на ведение карточки после звонка сократилось с 15-20 минут до нуля
  • Доля сделок с заполненным полем «Запись встречи» выросла с 40% до 100%
  • Новые менеджеры при подключении к сделке могут за 2 минуты понять контекст прошлых переговоров

Побочный эффект: руководитель получил возможность выборочно проверять качество демо через Grain прямо из карточки в Kommo - без дополнительных инструментов.

Для кого подходит

Интеграция решает реальную задачу в конкретных сценариях:

Команды с высоким объёмом звонков. Если у менеджера 4-6 демо в день, ручной перенос заметок из Grain в CRM занимает час рабочего времени. Это прямая потеря на операционке.

Sales-процессы с длинным циклом. Когда сделка идёт 3-6 месяцев и в ней участвуют несколько менеджеров, контекст прошлых встреч критичен. Транскрипты в карточке становятся институциональной памятью.

Компании с требованиями к compliance. В некоторых отраслях нужно документировать содержание переговоров. Автоматическое сохранение транскриптов решает это без дополнительных процессов.

Интеграция не подойдёт, если встречи проходят хаотично без привязки к конкретным сделкам, или если команда не пользуется Grain стабильно - в таком случае правильнее сначала выстроить дисциплину использования инструмента.

Похожим образом устроена интеграция Kommo с Fireflies.ai - там тоже транскрипты попадают в карточку через webhook, но маппинг работает через другой идентификатор. Если вы используете TL;DV, принцип аналогичный - саммари встреч в карточку через TL;DV описан отдельно.

Для встреч, созданных напрямую из Kommo, посмотрите на интеграцию с Google Meet - там обратная механика: встреча создаётся из карточки, а не прилетает в неё после.

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

Нужен ли отдельный сервер для webhook-обработчика?

Да, нужен публичный HTTPS-endpoint. Проще всего развернуть Flask или FastAPI-сервис на Railway, Fly.io или аналогичных платформах. Альтернатива - AWS Lambda с Function URL или Google Cloud Functions. Для теста в процессе разработки подойдёт ngrok.

Что если у одного email несколько сделок в Kommo?

Функция find_lead_by_participant_emails берёт последнюю сделку по ID. Если нужна другая логика (например, только активные сделки), добавьте фильтр по полю status_id при запросе GET /api/v4/leads.

Grain поддерживает верификацию webhook-подписи?

Да. При создании webhook Grain возвращает secret. Входящие запросы содержат заголовок X-Grain-Signature - HMAC-SHA256 от тела запроса с этим секретом. Верификацию нужно добавить в обработчик перед основной логикой.

Транскрипт слишком длинный, не помещается в заметку Kommo?

Kommo не имеет жёсткого лимита на текст заметки, но на практике удобнее обрезать транскрипт до 3-5 тысяч символов и давать ссылку на полный вариант в Grain. AI-саммари при этом передаётся целиком - оно компактно и содержит самое важное.

Работает ли это с Teams-встречами?

Grain поддерживает запись из Zoom, Google Meet и Microsoft Teams. Тип источника отражается в поле source payload-а (zoom, google_meet, teams). Логика webhook-обработчика одинакова для всех трёх.

Итог

Grain + Kommo через webhook - это примерно 150 строк Python-кода и один публичный endpoint. Результат: транскрипт и AI-саммари каждой встречи автоматически появляются в нужной карточке сделки в течение нескольких минут после окончания звонка.

Если у вас другой стек встреч или нужна более сложная логика маппинга (по нескольким воронкам, с дедупликацией, с уведомлениями о ненайденных сделках), мы делаем кастомные интеграции для Kommo под конкретный процесс.

Свяжитесь с нами, чтобы обсудить вашу задачу.

Ещё статьи

Все →