Симптомы: что происходит с вашими данными
Вы подключили Apollo к HubSpot через нативную интеграцию. Кажется, что всё работает: контакты из Apollo появляются в HubSpot. Но через несколько недель картина становится тревожной:
- В HubSpot есть «John Smith» из Acme Corp с email john@acme.com - и ещё один «John Smith» тоже с john@acme.com, созданный Apollo
- Сделки в HubSpot не связаны с activity из Apollo sequences
- В Apollo есть поле «Apollo Score» (AI-оценка перспективности контакта) - в HubSpot его нет
- В HubSpot timeline не видно, что контакт открывал письма в Apollo sequences
- SDR открывает карточку контакта в HubSpot - и не знает, что коллега уже написал ему из Apollo три дня назад
Всё это - прямые следствия архитектурных ограничений нативной интеграции Apollo + HubSpot.
Корневые причины проблем
1. Дубли контактов - матчинг только по email
Когда Apollo создаёт контакт в HubSpot (при добавлении в sequence), нативная интеграция проверяет дубли только по полю email. Если в HubSpot уже есть контакт john@acme.com - интеграция должна найти его и не создавать нового.
Проблема: HubSpot хранит email как primary + secondary. Apollo проверяет только primary email. Если контакт в HubSpot имеет корпоративный email как secondary (а личный - как primary), Apollo создаст дубль.
Вторая проблема: некоторые компании используют email aliases. john@acme.com и j.smith@acme.com - разные адреса с точки зрения нативной интеграции, но один человек. Apollo не выполняет матчинг по имени + домену компании.
2. Нет привязки к существующим HubSpot Deals
Native Apollo <-> HubSpot интеграция создаёт/обновляет Contact и Company. Deal в HubSpot она не трогает.
Результат: Apollo добавляет контакт в sequence «Enterprise Outreach». Этот же контакт уже имеет открытую сделку в HubSpot у другого менеджера. Нативная интеграция не видит эту связь. В HubSpot нет уведомления «коллега работает с этим лидом в Apollo».
3. Apollo custom properties не передаются в HubSpot
Apollo имеет набор собственных полей, которые ценны для приоритизации:
apollo_score- AI-оценка перспективности (0-100)seniority- уровень в компании (C-level, VP, Manager)linkedin_url- LinkedIn профильkeywords- технологии, которые использует компанияtechnologies- технологический стек (из Apollo Intent data)
Нативная интеграция синхронизирует базовые поля: First Name, Last Name, Email, Title, Company. Apollo-специфичные поля остаются только в Apollo.
4. Sequence activity не попадает в HubSpot engagement timeline
HubSpot Timeline - это история взаимодействий с контактом: звонки, emails, meetings. Нативная интеграция Apollo НЕ записывает в Timeline:
- Факт добавления в sequence
- Письма, отправленные из Apollo
- Открытия и клики писем Apollo
- Ответы на письма Apollo
SDR, открывая контакт в HubSpot, видит пустой Timeline - хотя коллега уже отправил 3 письма через Apollo и получил автоответ «out of office».
Анализ потерь данных
Оцените масштаб проблемы на вашей базе:
import os
import requests
from collections import defaultdict
HUBSPOT_TOKEN = os.environ["HUBSPOT_PRIVATE_APP_TOKEN"]
HUBSPOT_BASE = "https://api.hubapi.com"
def find_duplicate_contacts() -> dict:
"""Находим дубли контактов в HubSpot по email."""
headers = {"Authorization": f"Bearer {HUBSPOT_TOKEN}"}
email_to_contacts: defaultdict[str, list] = defaultdict(list)
after = None
while True:
params = {
"limit": 100,
"properties": "email,firstname,lastname,createdate,hs_analytics_source",
}
if after:
params["after"] = after
r = requests.get(
f"{HUBSPOT_BASE}/crm/v3/objects/contacts",
params=params,
headers=headers,
timeout=15,
)
if not r.ok:
break
data = r.json()
contacts = data.get("results", [])
for contact in contacts:
email = contact["properties"].get("email", "")
if email:
email_to_contacts[email.lower()].append({
"id": contact["id"],
"name": f"{contact['properties'].get('firstname', '')} {contact['properties'].get('lastname', '')}",
"created": contact["properties"].get("createdate"),
"source": contact["properties"].get("hs_analytics_source"),
})
paging = data.get("paging")
if paging and paging.get("next"):
after = paging["next"]["after"]
else:
break
duplicates = {
email: contacts
for email, contacts in email_to_contacts.items()
if len(contacts) > 1
}
return duplicates
duplicates = find_duplicate_contacts()
print(f"Дублей найдено: {len(duplicates)}")
for email, contacts in list(duplicates.items())[:5]:
print(f" {email}: {len(contacts)} записей")
for c in contacts:
print(f" ID={c['id']}, source={c['source']}, created={c['created']}")
Правильный подход: кастомная интеграция
Кастомная интеграция Apollo + HubSpot через API обеих систем решает все проблемы:
Шаг 1. Двусторонний матчинг при синхронизации контактов
import re
APOLLO_API_KEY = os.environ["APOLLO_API_KEY"]
APOLLO_BASE = "https://api.apollo.io/v1"
def search_hubspot_contact(email: str, domain: str, name: str) -> str | None:
"""Ищем контакт в HubSpot по email, затем по домену+имени."""
headers = {"Authorization": f"Bearer {HUBSPOT_TOKEN}"}
# Поиск по точному email
r = requests.post(
f"{HUBSPOT_BASE}/crm/v3/objects/contacts/search",
json={
"filterGroups": [{
"filters": [{
"propertyName": "email",
"operator": "EQ",
"value": email,
}]
}],
"properties": ["email", "firstname", "lastname", "associatedcompanyid"],
},
headers=headers,
timeout=10,
)
if r.ok:
results = r.json().get("results", [])
if results:
return results[0]["id"]
# Поиск по домену + имени (fallback)
if domain and name:
domain_r = requests.post(
f"{HUBSPOT_BASE}/crm/v3/objects/contacts/search",
json={
"filterGroups": [{
"filters": [
{"propertyName": "email", "operator": "CONTAINS_TOKEN", "value": f"*@{domain}"},
{"propertyName": "lastname", "operator": "EQ",
"value": name.split()[-1] if name.split() else ""},
]
}],
"properties": ["email", "firstname", "lastname"],
},
headers=headers,
timeout=10,
)
if domain_r.ok:
results = domain_r.json().get("results", [])
if results:
return results[0]["id"]
return None
def sync_apollo_contact_to_hubspot(apollo_contact: dict) -> str:
"""Синхронизируем Apollo-контакт в HubSpot с полным набором полей."""
email = apollo_contact.get("email", "")
domain = email.split("@")[1] if "@" in email else ""
name = f"{apollo_contact.get('first_name', '')} {apollo_contact.get('last_name', '')}".strip()
existing_id = search_hubspot_contact(email, domain, name)
headers = {
"Authorization": f"Bearer {HUBSPOT_TOKEN}",
"Content-Type": "application/json",
}
properties = {
"email": email,
"firstname": apollo_contact.get("first_name", ""),
"lastname": apollo_contact.get("last_name", ""),
"jobtitle": apollo_contact.get("title", ""),
"phone": apollo_contact.get("phone", ""),
"linkedin_url": apollo_contact.get("linkedin_url", ""),
# Кастомные свойства Apollo - должны быть созданы в HubSpot заранее
"apollo_score": str(apollo_contact.get("score", 0)),
"apollo_seniority": apollo_contact.get("seniority", ""),
"apollo_technologies": ", ".join(
apollo_contact.get("account", {}).get("technologies", [])[:10]
),
}
if existing_id:
# Обновляем существующий
r = requests.patch(
f"{HUBSPOT_BASE}/crm/v3/objects/contacts/{existing_id}",
json={"properties": properties},
headers=headers,
timeout=10,
)
return existing_id
else:
# Создаём новый
r = requests.post(
f"{HUBSPOT_BASE}/crm/v3/objects/contacts",
json={"properties": properties},
headers=headers,
timeout=10,
)
if r.ok:
return r.json()["id"]
return ""
def log_apollo_sequence_activity(
hubspot_contact_id: str,
sequence_name: str,
event_type: str,
email_subject: str,
occurred_at: str,
):
"""Записываем Apollo sequence activity в HubSpot Timeline."""
# Нужно создать Custom Timeline Event Type в HubSpot заранее
TIMELINE_EVENT_TYPE_ID = os.environ.get("HUBSPOT_APOLLO_TIMELINE_TYPE_ID", "")
APP_ID = os.environ.get("HUBSPOT_APP_ID", "")
if not TIMELINE_EVENT_TYPE_ID or not APP_ID:
return
r = requests.put(
f"{HUBSPOT_BASE}/integrations/v1/{APP_ID}/timeline/event",
headers={
"Authorization": f"Bearer {HUBSPOT_TOKEN}",
"Content-Type": "application/json",
},
json={
"eventTypeId": TIMELINE_EVENT_TYPE_ID,
"id": f"apollo_{sequence_name}_{event_type}_{hubspot_contact_id}_{occurred_at}",
"objectId": hubspot_contact_id,
"occurredAt": occurred_at,
"extraData": {
"sequence_name": sequence_name,
"event_type": event_type,
"email_subject": email_subject,
},
},
timeout=10,
)
return r.ok
Шаг 2. Привязка к существующей Deal
def associate_contact_to_deal_if_exists(
hubspot_contact_id: str, deal_search_email: str
):
"""Привязываем контакт к существующей открытой сделке если она есть."""
headers = {
"Authorization": f"Bearer {HUBSPOT_TOKEN}",
"Content-Type": "application/json",
}
# Ищем открытые сделки контакта
r = requests.get(
f"{HUBSPOT_BASE}/crm/v3/objects/contacts/{hubspot_contact_id}/associations/deals",
headers=headers,
timeout=10,
)
if r.ok:
deals = r.json().get("results", [])
if deals:
# Контакт уже привязан к сделке - не создаём дубль
print(f"Contact {hubspot_contact_id} already has {len(deals)} deal(s)")
return deals[0]["id"]
# Сделок нет - можно создать или просто оставить
return None
Для кого подходит кастомная интеграция
Кастомная интеграция Apollo + HubSpot нужна компаниям:
- С базой HubSpot от 5000 контактов, где проблема дублей уже ощутима
- Которые используют Apollo Intent Data и Apollo Score для приоритизации - и хотят эти данные видеть в HubSpot
- Где SDR работают в Apollo, а AE и CSM работают в HubSpot - нужна единая история
- Где compliance-требования (GDPR, SOC2) требуют полного аудита всех коммуникаций в одной системе
Если вас интересуют другие антипаттерны HubSpot - читайте про HubSpot + Zendesk и HubSpot + Slack.
Часто задаваемые вопросы
Нативная интеграция Apollo - это платная функция? HubSpot интеграция в Apollo доступна на планах Professional и выше. На Basic плане Apollo доступны только ручной экспорт CSV и базовая синхронизация через Zapier.
Можно ли очистить существующие дубли перед переходом на кастомную интеграцию?
Да. HubSpot имеет встроенный инструмент Merge Contacts (Settings -> Data Management -> Duplicates). Для массовой очистки используйте HubSpot API: найдите дубли скриптом выше, затем вызовите POST /crm/v3/objects/contacts/merge для каждой пары.
Apollo API - это платный доступ? Apollo REST API доступен на платных планах (Basic от $49/month). На бесплатном плане API доступа нет. Для кастомной интеграции нужен как минимум Basic план Apollo.
Как обеспечить GDPR-compliance при синхронизации данных? Apollo собирает данные из открытых источников. При синхронизации в HubSpot убедитесь, что у контакта есть lawful basis для обработки данных (legitimate interest для B2B prospecting в EU допустим, но должен быть задокументирован). Не синхронизируйте контакты без явного opt-in в страны с жёстким законодательством (Германия, Нидерланды).
Если у вас проблемы с нативной интеграцией HubSpot + Apollo - опишите симптомы команде Exceltic.dev. Разберём архитектуру и предложим решение без дублей и потери данных.