HubSpot + Loom: почему видео-просмотры не попадают в Deal Timeline и как это исправить

HubSpot и Loom имеют нативную интеграцию: вставка Loom-видео в HubSpot Email Sequences, отслеживание кликов. Команды продаж используют это для video prospecting: записал персональное видео - отправил через Sequence - видел что клиент посмотрел. Проблема: стандартная интеграция фиксирует только клик по превью в email, не сам факт просмотра видео. И самое важное - ни клик, ни просмотр не появляется в Deal Timeline. Активность видна в Contact Activity, но не привязана к сделке.

Это архитектурный ограничение нативной интеграции: Loom для HubSpot - плагин Chrome для вставки видео в письма, не двусторонняя API-интеграция. Loom не отправляет события в HubSpot при каждом просмотре видео.

Что теряет команда продаж

  • Менеджер не видит в карточке сделки: кто из команды клиента смотрел видео, когда, сколько раз
  • RevOps не может построить отчёт: сделки, где был просмотр Loom, конвертируются лучше?
  • Нет trigger для follow-up: “клиент посмотрел видео -> поставить задачу позвонить”

Это не “nice to have” - для команд активно использующих video selling (Loom в каждом touch) потеря контекста в Deal ломает весь workflow.

Почему нативная интеграция работает именно так

Loom нативная интеграция с HubSpot - это HubSpot Sales Extension: вставляет превью видео в тело письма через HubSpot Email editor. При клике по превью HubSpot фиксирует Email Click event. Это стандартная механика Email tracking - не специфика Loom.

Loom не уведомляет HubSpot о просмотрах на своей платформе. Данные о просмотрах (viewer email, watch percentage, rewatch) хранятся в Loom Analytics и недоступны HubSpot в реальном времени.

Правильный подход: Loom Webhook -> HubSpot Engagement

Loom предоставляет webhooks для событий: video.viewed, video.completed, video.shared. При настройке webhook Loom отправляет данные о просмотре на ваш endpoint - включая email зрителя (если известен) и video_id.

Loom webhook: video.viewed
  {viewer_email, video_id, watch_time_percentage, timestamp}
  -> Ваш сервер

Ваш сервер
  -> HubSpot API: найти Contact по viewer_email
  -> HubSpot API: найти открытую Deal для этого Contact
  -> HubSpot API: создать Engagement (Note) в Deal Timeline
     "Loom просмотрен: {video_title}, {watch_pct}%, {duration}s"

Реализация

import requests, os, hmac, hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)

HS_TOKEN     = os.environ["HUBSPOT_ACCESS_TOKEN"]
LOOM_SECRET  = os.environ["LOOM_WEBHOOK_SECRET"]
HS_BASE      = "https://api.hubapi.com"
HS_HDR       = {"Authorization": f"Bearer {HS_TOKEN}", "Content-Type": "application/json"}

def verify_loom_sig(body: bytes, sig: str) -> bool:
    if not LOOM_SECRET:
        return True
    expected = hmac.new(LOOM_SECRET.encode(), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(f"sha256={expected}", sig)

def find_contact_id(email: str) -> str | None:
    r = requests.post(
        f"{HS_BASE}/crm/v3/objects/contacts/search",
        headers=HS_HDR,
        json={
            "filterGroups": [{"filters": [{
                "propertyName": "email",
                "operator": "EQ",
                "value": email,
            }]}],
            "properties": ["email"],
            "limit": 1,
        },
    )
    results = r.json().get("results", []) or []
    return results[0]["id"] if results else None

def find_deal_for_contact(contact_id: str) -> str | None:
    r = requests.get(
        f"{HS_BASE}/crm/v3/objects/contacts/{contact_id}/associations/deals",
        headers=HS_HDR,
    )
    results = r.json().get("results", []) or []
    if not results:
        return None
    deal_id = results[0]["id"]
    # Проверить что сделка открытая
    rd = requests.get(
        f"{HS_BASE}/crm/v3/objects/deals/{deal_id}",
        headers=HS_HDR,
        params={"properties": "dealstage,closedate"},
    )
    stage = rd.json().get("properties", {}).get("dealstage", "")
    closed_stages = {"closedwon", "closedlost"}
    if stage in closed_stages:
        return None
    return deal_id

def create_note_in_deal(deal_id: str, note_text: str):
    # Шаг 1: создать Engagement (Note)
    r = requests.post(
        f"{HS_BASE}/crm/v3/objects/notes",
        headers=HS_HDR,
        json={
            "properties": {
                "hs_note_body":      note_text,
                "hs_timestamp":      str(int(__import__("time").time() * 1000)),
            },
        },
    )
    r.raise_for_status()
    note_id = r.json().get("id", "")

    # Шаг 2: привязать Note к Deal
    requests.put(
        f"{HS_BASE}/crm/v4/objects/notes/{note_id}/associations/deals/{deal_id}",
        headers=HS_HDR,
        json=[{"associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 214}],
    )

@app.route("/webhooks/loom", methods=["POST"])
def loom_webhook():
    sig = request.headers.get("X-Loom-Signature", "")
    if not verify_loom_sig(request.data, sig):
        return jsonify({"error": "invalid signature"}), 401

    event    = request.json or {}
    ev_type  = event.get("type", "")

    if ev_type not in ("video.viewed", "video.completed"):
        return jsonify({"status": "ignored"}), 200

    data         = event.get("data", {})
    viewer_email = data.get("viewer_email", "")
    video_title  = data.get("video_title", "Loom видео")
    watch_pct    = data.get("watch_percentage", 0)
    duration     = data.get("duration_seconds", 0)
    video_url    = data.get("video_url", "")

    if not viewer_email:
        return jsonify({"status": "no_email"}), 200

    contact_id = find_contact_id(viewer_email)
    if not contact_id:
        return jsonify({"status": "contact_not_found"}), 200

    deal_id = find_deal_for_contact(contact_id)
    if not deal_id:
        return jsonify({"status": "no_open_deal"}), 200

    pct_str = f"{watch_pct:.0f}%" if watch_pct else "неизвестно"
    note    = (
        f"Loom: {viewer_email} просмотрел видео '{video_title}' "
        f"({pct_str}, {duration}s). "
        f"{video_url}"
    )
    create_note_in_deal(deal_id, note)
    return jsonify({"status": "ok"}), 200

Настройка Loom Webhook

Loom Dashboard -> Settings -> Webhooks. Доступно на Loom Business и Enterprise планах. События: video.viewed, video.completed. URL: ваш endpoint. Secret: используется в X-Loom-Signature.

Loom отправляет webhook при каждом просмотре, включая повторные. Если клиент посмотрел видео 3 раза - 3 события. В note_text можно добавить (пересмотр) если watch_percentage был уже близок к 100% в предыдущем событии (нужна дополнительная логика дедупликации).

Trigger для follow-up задачи

def create_followup_task(contact_id: str, deal_id: str, viewer_email: str):
    r = requests.post(
        f"{HS_BASE}/crm/v3/objects/tasks",
        headers=HS_HDR,
        json={
            "properties": {
                "hs_task_body":    f"Follow up: {viewer_email} посмотрел Loom видео. Время позвонить.",
                "hs_task_subject": "Позвонить после просмотра Loom",
                "hs_task_status":  "NOT_STARTED",
                "hs_task_type":    "CALL",
                "hs_timestamp":    str(int(__import__("time").time() * 1000) + 3600000),  # +1h
            },
        },
    )
    task_id = r.json().get("id", "")
    if task_id and deal_id:
        requests.put(
            f"{HS_BASE}/crm/v4/objects/tasks/{task_id}/associations/deals/{deal_id}",
            headers=HS_HDR,
            json=[{"associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 216}],
        )

Для кого актуально

Sales-команды HubSpot, активно использующие Loom для personalized video prospecting. Особенно если video selling - основной touchpoint в email sequences: нужна история просмотров в Deal Timeline для оценки вовлечённости и построения follow-up задач.

Аналогичный антипаттерн разобран для HubSpot + Drift.

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

Передаёт ли Loom email зрителя в webhook всегда?

Нет. viewer_email присутствует только если зритель авторизован в Loom при просмотре, или если ссылка открыта из email-клиента с tracking. Если видео встроено на сайте или открыто анонимно - email будет пустым. Для prospecting (персональные ссылки в emails) зритель обычно идентифицируется.

Как работать с несколькими открытыми сделками для одного контакта?

find_deal_for_contact в примере берёт первую. Для точной привязки: в Loom webhook добавьте кастомный параметр hs_deal_id в URL видео (через Loom Custom Link Parameters, Enterprise план). Или привяжите видео к конкретной сделке через кастомное поле при отправке.

Нужен ли HubSpot Enterprise для этой интеграции?

Нет. HubSpot Notes API (создание Note + привязка к Deal) работает на всех paid планах HubSpot CRM (Starter и выше). Loom webhook доступен на Business и Enterprise планах Loom ($12.50/user/mo и выше).

Итог

HubSpot + Loom - видео-события в Deal Timeline:

  • Нативная интеграция: только Email Click, не просмотр видео
  • Loom webhook video.viewed -> find Contact by email -> find open Deal -> Note
  • Note в Deal через CRM v3: POST /crm/v3/objects/notes + PUT associations/deals
  • associationTypeId: 214 для Note -> Deal
  • Опционально: CREATE TASK через associationTypeId: 216 Task -> Deal для follow-up

Если нужна интеграция Loom с HubSpot Deal Timeline - опишите задачу команде Exceltic.dev.

Ещё статьи

Все →