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 под конкретный процесс.
Свяжитесь с нами, чтобы обсудить вашу задачу.