Интеграция HubSpot и n8n: контакты, сделки и дедупликация по email/phone ¶
Обновлено: 2026-05-30
Импортируйте JSON в n8n, замените credentials, URL API, project/list IDs, поля и лимиты под вашу инфраструктуру.
Проблема: HubSpot даёт мощную CRM-модель, но прямой create contact/create deal из n8n без поиска дублей создаёт разрозненные контакты и сделки без associations.
Решение: Нужно использовать n8n как слой качества данных: нормализовать email/phone, искать contact, создавать или обновлять его, затем создавать deal и association только по явному бизнес-правилу.
Проблема: почему простая интеграция создаёт дубли и ручной хаос ¶
HubSpot часто подключают к формам, лендингам, чат-ботам и платежам. Данные выглядят простыми: имя, email, телефон, источник. Но в CRM важна модель объектов: contact, company, deal, associations, pipeline, stage и properties.
Если workflow создаёт контакт и сделку без проверки, отдел продаж получает несколько карточек одного человека, маркетинг теряет атрибуцию, а автоматизация email nurture срабатывает не на тот lifecycle stage. Эта страница закрывает задачу интегратора: не просто отправить лид в HubSpot, а сохранить CRM-структуру.
Архитектура workflow для n8n ¶
| Блок | Задача | Production-проверка |
|---|---|---|
| Webhook / lead source | принимает форму, чат или событие оплаты | source, event_id, email/phone |
| Normalize contact | чистит email, phone, name и UTM | валидный email или phone |
| Search HubSpot contact | ищет существующий contact по email/phone | поиск до create |
| Upsert contact | обновляет properties или создаёт contact | lifecycle stage и source |
| Create/associate deal | создаёт deal и связывает с contact | pipeline, dealstage, association |
| Respond / audit | возвращает статус и пишет журнал | без токенов и PII в alert |
Для HubSpot важно отдельно думать про contact и deal. Повторная заявка может обновлять contact, но создавать новую deal только при новом order_id, request_id или бизнес-событии.
Контракт входных данных ¶
{
"event_id": "lead-8842",
"email": "maria@example.com",
"phone": "+7 (925) 600-10-20",
"firstname": "Мария",
"lastname": "Иванова",
"company": "Acme RU",
"deal_name": "Внедрение n8n для отдела продаж",
"amount": 240000,
"pipeline": "default",
"dealstage": "appointmentscheduled",
"utm_source": "google",
"utm_medium": "cpc",
"utm_campaign": "hubspot_n8n",
"source_url": "https://example.ru/hubspot"
}Минимальный контракт — email или phone плюс event_id. Для deal нужен отдельный ключ события: иначе каждое обновление contact может случайно создать новую сделку.
Code Node: нормализация, mapping и guard-условия ¶
const src = $json.body ?? $json;
const email = String(src.email ?? '').trim().toLowerCase();
const rawPhone = String(src.phone ?? '').trim();
let digits = rawPhone.replace(/\D/g, '');
if (digits.length === 11 && digits.startsWith('8')) digits = `7${digits.slice(1)}`;
if (digits.length === 10) digits = `7${digits}`;
if (!email && !digits) throw new Error('HubSpot contact needs email or phone');
const eventId = String(src.event_id ?? src.order_id ?? '').trim();
if (!eventId) throw new Error('No event_id for HubSpot sync');
const contactKey = email || `phone:+${digits}`;
const dealKey = `hubspot-deal:${eventId}`;
return [{ json: {
contact_key: contactKey,
deal_key: dealKey,
contact_properties: {
email,
phone: digits ? `+${digits}` : '',
firstname: String(src.firstname ?? src.name ?? '').trim(),
lastname: String(src.lastname ?? '').trim(),
company: String(src.company ?? '').trim(),
lifecyclestage: 'lead',
hs_analytics_source: String(src.utm_source ?? '').trim()
},
deal_properties: {
dealname: String(src.deal_name ?? `Заявка ${eventId}`).trim(),
amount: Number(src.amount ?? 0),
pipeline: String(src.pipeline ?? 'default').trim(),
dealstage: String(src.dealstage ?? '').trim(),
description: `Source: ${src.source_url ?? ''}; campaign: ${src.utm_campaign ?? ''}`
},
idempotency_key: `hubspot:${eventId}`
}}];
Почему contact и deal нельзя считать одним объектом
Contact описывает человека, а deal — конкретную возможность или заказ. Повторная форма может обновлять контакт, но новая сделка должна создаваться только при новом бизнес-событии.
Готовый workflow JSON: скачать и импортировать ¶
Скачать готовый workflow JSON Скачать тестовый payload
{
"name": "Nodbot - HubSpot contact and deal sync with dedupe",
"nodes": [
{
"name": "HubSpot Lead Webhook",
"type": "n8n-nodes-base.webhook",
"purpose": "Принять lead event"
},
{
"name": "Normalize HubSpot fields",
"type": "n8n-nodes-base.code",
"purpose": "Собрать contact/deal properties"
},
{
"name": "Search HubSpot contact",
"type": "n8n-nodes-base.httpRequest",
"purpose": "Найти контакт"
},
{
"name": "Upsert contact",
"type": "n8n-nodes-base.httpRequest",
"purpose": "Создать или обновить contact"
},
{
"name": "Create and associate deal",
"type": "n8n-nodes-base.httpRequest",
"purpose": "Создать deal и association"
},
{
"name": "Respond",
"type": "n8n-nodes-base.respondToWebhook",
"purpose": "Вернуть статус"
}
],
"connections": "HubSpot Lead Webhook → Normalize HubSpot fields → Search HubSpot contact → Upsert contact → Create and associate deal → Respond"
}
Пошаговая настройка связки ¶
- Создайте private app token HubSpot с минимальными CRM scopes.
- Импортируйте workflow JSON и замените portal-specific properties.
- Определите правило: когда обновлять contact, а когда создавать новую deal.
- Проверьте pipeline и dealstage на тестовом портале.
- Отправьте два одинаковых payload и проверьте дубли contact/deal.
Тесты перед production ¶
curl -X POST "https://YOUR-N8N-DOMAIN/webhook/integration-hubspot-n8n-contact-deal-sync" \
-H "Content-Type: application/json" \
--data @integration-hubspot-n8n-contact-deal-sync-payload.json- Повторный payload не создаёт дубль и возвращает тот же output key.
- Некорректный mapping останавливается до запроса к внешнему API.
- Пустые необязательные поля не ломают workflow.
- Ошибка API уходит в alert или DLQ с безопасным payload.
- Execution data не содержит секретов, токенов и лишних персональных данных.
Production-риски ¶
- Deal создаётся на каждое обновление. CRM получает фальшивую воронку.
- Associations забыты. Сделка есть, но не связана с contact/company.
- Поиск только по email. В части российских лидов email пустой или технический.
- Неверный lifecycle stage. Маркетинговые сценарии запускаются не по тому сегменту.
- Слишком широкий token. Private app должен иметь минимально нужные CRM-разрешения.
Полезные ссылки и смежные материалы ¶
- HubSpot CRM APIs overview
- HubSpot Contacts API
- HubSpot Deals API
- Webhook idempotency to Postgres
- amoCRM и n8n
Критерии готовности ¶
- Повторная заявка не создаёт второй contact.
- Deal создаётся только по новому event_id или order_id.
- Contact и deal связаны association.
- Pipeline/dealstage протестированы на портале.
- Ошибки HubSpot API уходят в alert или DLQ.
Nodbot настроит HubSpot + n8n: дедупликацию контактов, deal rules, associations, UTM, alert, retry и тесты на реальных payload.
Обсудить HubSpot-интеграцию