HubSpot + Salesloft: почему нативная интеграция теряет контекст сделки в кадансах
Salesloft — leading sales engagement платформа: кадансы (автоматические последовательности звонков/email/LinkedIn), conversation intelligence, deal insights. Нативная интеграция с HubSpot существует и работает из коробки. Но «работает» не значит «правильно» — в типовых enterprise-командах нативная интеграция создаёт три системных проблемы которые невидимы в первые месяцы и болезненны через полгода.
Три проблемы нативной HubSpot + Salesloft интеграции
1. Звонки и письма попадают в Activity, а не в Timeline сделки
Нативная интеграция пишет Salesloft-активности в HubSpot Contact Activity stream. Менеджер открывает сделку (Deal) — и не видит ни одного письма из Salesloft-каданса. Нужно идти в Contact -> Activity и листать всё что когда-либо делалось с этим контактом.
Результат: менеджер по продажам не имеет контекста сделки в момент звонка. Deal timeline пустой. История коммуникации — в другом месте.
2. Кадансы запускаются без проверки стадии Deal
Salesloft запускает каданс на Contact по триггеру (добавлен в лист, тег в CRM). Нативная интеграция не проверяет текущую стадию связанной Deal в HubSpot. Контакт может уже находиться в стадии «Negotiation» или «Closed Won» — а Salesloft продолжает отправлять ему первичный prospecting-каданс.
Результат: клиент которому уже выставлен счёт получает письмо «Привет, вы ещё не знакомы с нашим продуктом?». Репутационный и коммерческий риск.
3. Дубли активностей при двустороннем sync
Нативная интеграция синхронизирует активности в обе стороны. Если менеджер отвечает на письмо из HubSpot — в Salesloft появляется эта активность. Salesloft считает каданс-шаг выполненным и двигает дальше. При следующей sync HubSpot получает дубль активности от Salesloft.
Результат: дублированные записи в Contact activity, некорректная аналитика по email open rates и reply rates.
Почему нативная интеграция устроена именно так
Salesloft и HubSpot интегрированы через Contact-объект как общий знаменатель. Это логично с точки зрения архитектуры: Contact существует в обеих системах, email и phone — его атрибуты. Но в HubSpot продажи ведутся через Deal-объект, не Contact. Deal имеет свой pipeline stage, свой owner, свою timeline.
Нативная интеграция не имеет механизма «привязать активность Salesloft к конкретной Deal». HubSpot API позволяет это через Association API — но нативный коннектор Salesloft не использует его при синхронизации активностей.
Что конкретно теряет бизнес
- Deal context: менеджер перед звонком не видит историю переписки из каданса
- Cadence hygiene: кадансы запускаются на контактах из Won/Lost deals
- Analytics accuracy: reply rate в Salesloft не совпадает с email activity в HubSpot
- Manager visibility: руководитель не может посмотреть «какие кадансы работают по каким стадиям»
В типичной SDR-команде (10 человек, 500+ контактов в активных кадансах) это означает 3–5 часов в неделю на ручную «уборку» — удаление дублей, остановку кадансов на Won контактах, поиск истории в двух местах.
Правильная архитектура: HubSpot API + Salesloft API
Решение — кастомная bidirectional интеграция через API обеих платформ:
import requests
HS_TOKEN = "your_hubspot_private_app_token"
SL_TOKEN = "your_salesloft_api_token"
HS_BASE = "https://api.hubapi.com"
SL_BASE = "https://api.salesloft.com/v2"
HS_HEADERS = {
"Authorization": f"Bearer {HS_TOKEN}",
"Content-Type": "application/json",
}
SL_HEADERS = {
"Authorization": f"Bearer {SL_TOKEN}",
"Content-Type": "application/json",
}
def get_hs_deal_stage(deal_id: str) -> str:
resp = requests.get(
f"{HS_BASE}/crm/v3/objects/deals/{deal_id}",
headers=HS_HEADERS,
params={"properties": "dealstage,pipeline"},
)
resp.raise_for_status()
return resp.json().get("properties", {}).get("dealstage", "")
def associate_activity_to_deal(activity_id: str, deal_id: str):
# Правильный путь: привязать Salesloft-активность к Deal через HubSpot Association API
resp = requests.put(
f"{HS_BASE}/crm/v3/objects/notes/{activity_id}/associations/deals/{deal_id}/note_to_deal",
headers=HS_HEADERS,
)
resp.raise_for_status()
def create_hs_note_on_deal(deal_id: str, body: str,
activity_type: str = "EMAIL") -> str:
# Создать Note на Deal (не на Contact) - появляется в Deal Timeline
payload = {
"properties": {
"hs_note_body": body,
"hs_timestamp": str(int(__import__("time").time() * 1000)),
"hs_activity_type": activity_type,
},
"associations": [
{
"to": {"id": deal_id},
"types": [{"associationCategory": "HUBSPOT_DEFINED",
"associationTypeId": 214}],
}
],
}
resp = requests.post(
f"{HS_BASE}/crm/v3/objects/notes",
headers=HS_HEADERS,
json=payload,
)
resp.raise_for_status()
return resp.json().get("id", "")
def should_start_cadence(contact_id: str, cadence_type: str) -> bool:
# Проверить стадию Deal перед запуском каданса
deals = get_hs_contact_deals(contact_id)
BLOCKED_STAGES = {"closedwon", "closedlost", "contractsent", "decisionmakerboughtin"}
for deal in deals:
stage = get_hs_deal_stage(deal["id"])
if stage.lower() in BLOCKED_STAGES:
return False
return True
def get_hs_contact_deals(contact_id: str) -> list:
resp = requests.get(
f"{HS_BASE}/crm/v3/objects/contacts/{contact_id}/associations/deals",
headers=HS_HEADERS,
)
resp.raise_for_status()
return resp.json().get("results", [])
Salesloft webhook -> HubSpot Deal Timeline
@app.route("/webhooks/salesloft", methods=["POST"])
def salesloft_webhook():
payload = request.json
event_type = payload.get("event_type", "")
data = payload.get("data", {})
if event_type in ("email_sent", "call_completed", "email_replied"):
person_id = data.get("person", {}).get("id")
sl_email = data.get("person", {}).get("email_address", "")
# Найти HubSpot Contact по email
hs_contact = find_hs_contact_by_email(sl_email)
if not hs_contact:
return "", 200
contact_id = hs_contact["id"]
deals = get_hs_contact_deals(contact_id)
if not deals:
return "", 200
# Привязать активность к первой открытой Deal
deal_id = deals[0]["id"]
summary = format_salesloft_activity(event_type, data)
create_hs_note_on_deal(deal_id, summary, activity_type="CALL" if "call" in event_type else "EMAIL")
elif event_type == "cadence_person_added":
person_email = data.get("person", {}).get("email_address", "")
cadence_name = data.get("cadence", {}).get("name", "")
# Проверить - можно ли запускать каданс на этот контакт
hs_contact = find_hs_contact_by_email(person_email)
if hs_contact:
if not should_start_cadence(hs_contact["id"], cadence_name):
# Остановить каданс через Salesloft API
person_id = data.get("person", {}).get("id")
stop_salesloft_cadence(person_id)
log_blocked_cadence(person_email, cadence_name)
return "", 200
def stop_salesloft_cadence(person_id: str):
resp = requests.post(
f"{SL_BASE}/cadence_memberships",
headers=SL_HEADERS,
json={"person_id": person_id, "action": "remove"},
)
# Не raise_for_status - если уже нет в кадансе, это ок
def format_salesloft_activity(event_type: str, data: dict) -> str:
if event_type == "email_sent":
subject = data.get("email_body", {}).get("subject", "")
cadence = data.get("cadence", {}).get("name", "")
return f"Salesloft: отправлено письмо [{cadence}] - тема: {subject}"
elif event_type == "call_completed":
duration = data.get("call", {}).get("duration", 0)
outcome = data.get("call", {}).get("sentiment", "")
return f"Salesloft: звонок {duration} сек., итог: {outcome}"
elif event_type == "email_replied":
return f"Salesloft: получен ответ на письмо из каданса"
return f"Salesloft: {event_type}"
Реальный кейс
Enterprise SaaS (US, SDR-команда 12 человек, HubSpot + Salesloft):
- Проблема: 15–20% контактов в кадансах оказывались из Won/Lost Deals — получали prospecting-письма как новые лиды. Нашли через жалобу существующего клиента.
- Решение: кастомная прослойка:
cadence_person_addedwebhook -> проверка Deal stage -> stop cadence если Won/Lost/ContractSent. - Дополнительно: Salesloft call recordings -> создаются Notes на Deal через HubSpot API. Руководитель видит всю историю каданса в Deal Timeline без переключения на Contact Activity.
- Результат: 0 случаев отправки prospecting-кадансов Won клиентам за 3 месяца. Deal Timeline заполнен Salesloft-активностями.
Для кого актуально
- SDR/BDR команды на HubSpot + Salesloft где кадансы идут параллельно с активными сделками
- RevOps-специалисты которые строят reporting по связке Salesloft performance -> Deal stage
- Компании где клиенты жаловались на дублированные или неуместные письма из каданса
- Enterprise-продажи с длинным циклом где Deal Timeline — основной источник truth для менеджера
Часто задаваемые вопросы
Salesloft имеет официальную интеграцию с HubSpot — зачем кастомная?
Официальная интеграция Salesloft + HubSpot синхронизирует Contact-данные и пишет базовые активности в HubSpot Contact. Она не привязывает активности к Deal Timeline и не проверяет Deal Stage перед запуском каданса. Для команд с простым процессом этого достаточно. Для команд с enterprise-сделками и длинным циклом — нет.
Salesloft webhooks — как настроить?
Salesloft -> Settings -> Webhooks -> Add Webhook. Выбрать события: email.sent, call.completed, email.replied, cadence.membership.created. Указать endpoint URL. Salesloft отправляет POST с JSON payload. Аутентификация входящих — через Shared Secret в заголовке x-salesloft-signature.
Как избежать дублей активностей при кастомной интеграции?
Ключ идемпотентности: хранить salesloft_activity_id в HubSpot Note custom property. Перед созданием Note — проверять нет ли уже Note с этим salesloft_activity_id. Это предотвращает дубли при повторных webhook delivery (Salesloft может доставить webhook дважды при таймаутах).
HubSpot Association API — какой тип ассоциации Note -> Deal?
HubSpot Association Type ID для Note -> Deal: 214. При создании Note через POST /crm/v3/objects/notes передавать в associations объект с associationTypeId: 214. Это гарантирует что Note появится в Deal Timeline, а не только в Contact Activity.
Итого
- Нативная интеграция: активности -> Contact Activity (не Deal Timeline)
- Главный риск: кадансы на Won/Lost контактах без проверки стадии сделки
- Правильная архитектура: Salesloft webhook -> проверка HubSpot Deal Stage -> создание Note на Deal через Association API
- Идемпотентность: хранить
salesloft_activity_idв HubSpot чтобы избежать дублей - Association Type ID Note -> Deal:
214
Если у вас HubSpot + Salesloft и вы видите описанные симптомы — опишите ваш objём кадансов и количество SDR. Exceltic.dev аудирует текущую интеграцию и перестроит её с правильной привязкой к Deal Timeline.