HubSpot + Outreach: почему нативная интеграция теряет контекст сделок в последовательностях
Outreach - ведущая платформа для sales engagement: последовательности писем, звонков и LinkedIn-задач для SDR-команд. HubSpot - CRM, в которой AE ведут сделки. Нативная интеграция двух систем существует и активно продаётся обоими вендорами. На практике она работает нестабильно в трёх сценариях, которые критичны для большинства B2B-команд.
Главная проблема нативной интеграции: она синхронизирует контакты и активности, но не понимает Deal как объект. В результате менеджер видит в HubSpot, что проспект открывал письма, но не видит, в какой последовательности и на каком шаге отвалился.
Что происходит: три сценария поломки
Сценарий 1 - дублирование активностей в Timeline
Outreach отправляет активности (открытия писем, клики, ответы) в HubSpot через нативную интеграцию. Одновременно HubSpot имеет собственный email tracking через Sequences или Sales Hub. Если SDR использует Outreach, а письма идут с того же домена что и HubSpot-tracking, в Timeline контакта появляются дубли: одно и то же открытие письма записывается дважды.
Последствие: AE не может понять реальную картину вовлечённости. В Timeline 20 «открытий» - реально было 10. Скоринг контакта завышен.
Сценарий 2 - активности не привязываются к Deal
Нативная интеграция Outreach -> HubSpot создаёт активности (email_activity, call_activity) на уровне Contact, но не привязывает их к Deal. Если у контакта несколько сделок (например, разные продукты), непонятно, к какой относятся активности Outreach.
Последствие: Deal Timeline в HubSpot пустой. AE не видит, что SDR уже отправил 5 писем. AE отправляет ещё одно «вводное» письмо - проспект раздражён.
Сценарий 3 - конфликт при синхронизации свойств контакта
Outreach и HubSpot синхронизируют поля контакта в обе стороны. Это создаёт конфликты:
- SDR обновляет
job_titleв Outreach (взял из LinkedIn). HubSpot перезаписывает обратно старым значением из CRM. - AE меняет
lifecycle_stageв HubSpot наCustomer. Outreach продолжает включать контакт в SDR-последовательности (не получил обновление вовремя). - Поле
owner(ответственный) синхронизируется по email пользователя. Если в Outreach email пользователя не совпадает с HubSpot - владелец сделки назначается неверно.
Последствие: клиент, которого AE уже ведёт как активную сделку, продолжает получать SDR-письма «знакомства».
Почему так устроена нативная интеграция
Outreach и HubSpot строили интеграцию на основе общего объекта - Contact. Это правильно для синхронизации базовой информации, но недостаточно для B2B-продаж, где вся логика строится вокруг Deal.
Техническая причина: HubSpot Engagements API (теперь - Activities API) позволяет привязывать активности и к Contact, и к Deal одновременно. Нативная интеграция Outreach использует только привязку к Contact - это проще и быстрее в реализации, но теряет Deal-контекст.
Причина конфликтов: нативная интеграция использует polling (проверка изменений каждые N минут) вместо webhook-based синхронизации. Polling создаёт временной лаг и гонки данных.
Что теряет бизнес
Потеря 1 - slipped deals. AE не видит полной активности SDR по сделке. Принимает решения без контекста: «проспект не отвечает» - хотя в Outreach видно, что открывал 6 писем подряд, но ни разу не кликнул. Это другой диагноз и другая тактика.
Потеря 2 - damaged relationships. Клиент получает SDR-проспектинговые письма спустя месяц после того, как стал активным. Доверие к бренду падает. По данным Outreach (2024), 23% потерянных сделок объяснялись «ощущением, что нас атакуют письмами».
Потеря 3 - неверный attribution. Reporting в HubSpot не видит Outreach-активности как связанные с Deal. Атрибуция pipeline на SDR-активности искажена. Директор по продажам не может оценить ROI команды SDR.
Правильный подход: кастомная интеграция через API
Архитектура:
- Отказ от нативной двусторонней синхронизации контактов.
- Outreach -> HubSpot: только через Outreach webhook, только нужные события.
- Каждое событие Outreach записывается в HubSpot как Engagement, привязанный и к Contact, и к Deal (через Association).
- HubSpot -> Outreach: только изменение lifecycle_stage и deal_stage, через HubSpot webhook.
import requests
from typing import Optional
HS_BASE = "https://api.hubapi.com"
def find_deal_for_contact(hs_token: str, contact_id: str) -> Optional[str]:
"""Ищем активную сделку для контакта."""
resp = requests.get(
f"{HS_BASE}/crm/v3/objects/contacts/{contact_id}/associations/deals",
headers={"Authorization": f"Bearer {hs_token}"}
)
if resp.status_code != 200:
return None
results = resp.json().get("results", [])
# Берём первую активную (не closed) сделку
for assoc in results:
deal_id = assoc["id"]
deal = requests.get(
f"{HS_BASE}/crm/v3/objects/deals/{deal_id}",
headers={"Authorization": f"Bearer {hs_token}"},
params={"properties": "dealstage,closedate"}
).json()
stage = deal.get("properties", {}).get("dealstage", "")
if stage not in ["closedwon", "closedlost"]:
return deal_id
return None
def log_outreach_activity_to_hubspot(
hs_token: str,
contact_id: str,
deal_id: Optional[str],
activity_type: str, # "EMAIL_OPEN", "EMAIL_REPLY", "CALL"
body: str,
sequence_name: str,
step_number: int
):
"""Пишем активность Outreach в HubSpot Engagements."""
engagement_type_map = {
"EMAIL_OPEN": "EMAIL",
"EMAIL_REPLY": "EMAIL",
"CALL": "CALL",
"LINKEDIN_TASK": "TASK",
}
hs_type = engagement_type_map.get(activity_type, "NOTE")
full_body = f"[Outreach] {activity_type}\nПоследовательность: {sequence_name}\nШаг: {step_number}\n\n{body}"
associations = [{"objectType": "contact", "objectId": contact_id}]
if deal_id:
associations.append({"objectType": "deal", "objectId": deal_id})
payload = {
"engagement": {"type": hs_type, "timestamp": int(__import__('time').time() * 1000)},
"associations": associations,
"metadata": {"body": full_body},
}
resp = requests.post(
f"{HS_BASE}/engagements/v1/engagements",
headers={
"Authorization": f"Bearer {hs_token}",
"Content-Type": "application/json"
},
json=payload
)
resp.raise_for_status()
def handle_outreach_webhook(event: dict, hs_token: str):
"""Обрабатываем событие из Outreach и пишем в HubSpot."""
event_type = event["event_type"] # email.replied, call.completed и т.д.
prospect_email = event["prospect"]["email"]
# Находим контакт в HubSpot
search = requests.post(
f"{HS_BASE}/crm/v3/objects/contacts/search",
headers={"Authorization": f"Bearer {hs_token}", "Content-Type": "application/json"},
json={"filterGroups": [{"filters": [{"propertyName": "email", "operator": "EQ", "value": prospect_email}]}]}
).json()
contacts = search.get("results", [])
if not contacts:
return # Контакт не найден
contact_id = contacts[0]["id"]
deal_id = find_deal_for_contact(hs_token, contact_id)
log_outreach_activity_to_hubspot(
hs_token=hs_token,
contact_id=contact_id,
deal_id=deal_id,
activity_type=event_type.upper().replace(".", "_"),
body=event.get("body", ""),
sequence_name=event.get("sequence", {}).get("name", "Unknown"),
step_number=event.get("step_number", 0)
)
Пошаговая реализация правильной интеграции
Шаг 1 - отключить нативную синхронизацию. В настройках нативной интеграции Outreach -> HubSpot отключаете автоматическую синхронизацию активностей. Оставляете только sync контактов (если нужно) с явным direction: только Outreach -> HubSpot, не в обе стороны.
Шаг 2 - настроить Outreach webhooks. В Outreach Settings -> Webhooks добавляете URL микросервиса. Выбираете события: prospect.reply, call.completed, sequence.finished, sequence.bounced.
Шаг 3 - логика поиска Deal. Для каждого события Outreach сервис ищет Deal в HubSpot по Contact. Если несколько сделок - берём последнюю активную. Логику поиска настраиваете под ваш процесс.
Шаг 4 - привязка к Deal. Engagement создаётся с ассоциациями и к Contact, и к Deal. Это заполняет Deal Timeline.
Шаг 5 - HubSpot -> Outreach (lifecycle_stage). Подписываетесь на HubSpot webhook contact.propertyChange для свойства lifecyclestage. При смене на customer - через Outreach API обновляете prospect.stage = finished, что останавливает все активные последовательности.
Реальный кейс: B2B SaaS, 35 человек
SaaS-компания, 4 SDR в Outreach, 5 AE в HubSpot. Нативная интеграция работала 8 месяцев. Проблемы накапливались постепенно: в какой-то момент директор продаж заметил, что Deal Timeline у половины сделок пустой, хотя SDR отправляли письма.
После кастомной интеграции: каждое письмо, звонок и LinkedIn-задача из Outreach появляется в Deal Timeline HubSpot с указанием последовательности и шага. AE видит полную картину. Когда AE меняет lifecycle_stage на Customer - Outreach-кампания останавливается автоматически в течение 2 минут.
Цифры: 0 «лишних» писем клиентам за 4 месяца. Deal Timeline заполнен в 98% активных сделок (было 45%). Время AE на «ручное сведение» данных из Outreach и HubSpot сократилось с 30 мин/день до нуля.
Для кого актуально
Проблема касается любой команды, где SDR работают в Outreach, а AE - в HubSpot. Особенно болезненно:
- При масштабировании SDR-команды (с 1 до 4+ человек)
- При наличии нескольких продуктов и множества сделок на один контакт
- При строгих требованиях к атрибуции pipeline и reporting для борда
Часто задаваемые вопросы
Нативная интеграция Outreach + HubSpot - это платно?
Нативная интеграция включена в обоими продуктами. Платный Outreach в любом случае нужен для работы. HubSpot Sales Hub Professional или Enterprise - для полного доступа к Engagements API. Кастомная интеграция - дополнительная стоимость разработки, но окупается за счёт экономии времени команды.
SalesLoft vs Outreach - та же проблема с HubSpot?
Да. Разбор интеграции HubSpot + Salesloft показывает схожие проблемы: нативная интеграция не привязывает каденс-активности к Deal. Архитектурный подход к исправлению идентичен.
Можно ли сохранить нативную синхронизацию контактов и только активности делать кастомными?
Да. Нативная синхронизация контактов Outreach <-> HubSpot в целом работает нормально для базовых свойств. Проблема именно в активностях и Deal-привязке. Можно оставить нативный sync контактов и добавить кастомный слой только для активностей.
Как исключить дубли если нативная интеграция уже записала часть активностей?
При переходе на кастомную интеграцию нужен период «чистки»: отключить нативный sync активностей, отметить границу (дату переключения), не дублировать исторические активности. Старые дубли в HubSpot Timeline приходится чистить вручную или через bulk API-запросы.
Outreach умеет получать данные из HubSpot для персонализации писем?
Да. Outreach поддерживает Variables - переменные для персонализации, которые берутся из свойств Contact/Account. При правильной синхронизации ключевых полей из HubSpot в Outreach (через API, не нативно) SDR могут персонализировать письма данными из CRM.
Следующий шаг
Если у вас HubSpot + Outreach и вы сталкивались с пустым Deal Timeline или «лишними» письмами клиентам - опишите задачу команде Exceltic.dev. Разберём вашу архитектуру и предложим кастомную интеграцию с правильной привязкой активностей к Deal. Типовой проект - 2-3 недели.