HubSpot + Asana: почему нативная интеграция прячет задачи от sales-команды

Нативная интеграция HubSpot + Asana создаёт задачи в Asana из HubSpot, но не показывает выполненные задачи в Deal Activity Timeline. Менеджер по продажам заходит в сделку - и не видит, что ops-команда уже выполнила 3 из 5 onboarding-задач. Вся коммуникация про статус идёт через Slack или email - вместо одного источника правды в CRM.

Правильное решение: Asana webhook при завершении задачи -> ваш сервер -> HubSpot Note с привязкой к Deal через associationTypeId: 214. После этого каждое завершение Asana-задачи мгновенно появляется в Deal Timeline без ручного действия.

HubSpot Deal Activity Timeline - лента активностей внутри сделки: звонки, emails, notes, встречи. Основной инструмент sales-команды для понимания текущего статуса клиента. Если событие не появилось в Timeline - для менеджера оно не произошло.

Почему нативная интеграция не решает задачу

HubSpot и Asana имеют официальную интеграцию. Она позволяет:

  • Создавать задачи Asana из HubSpot (при создании Deal, вручную)
  • Видеть статус Asana-задачи в виджете на странице Deal

Чего она не делает:

  • Не добавляет события завершения задачи в Activity Timeline
  • Не создаёт Notes или Activities в HubSpot при изменениях в Asana
  • Не позволяет настроить кастомные триггеры (только при создании Deal)

Менеджер может посмотреть виджет - но это не то же самое, что видеть таймлайн с “Задача ‘Настроить интеграцию’ завершена Ивановым 12.06.2026 в 14:30”.

Что теряет бизнес

В типовом B2B SaaS с onboarding-циклом 30 дней: 5-7 задач в Asana создаются при выигрыше сделки. Sales-менеджер должен знать статус клиента перед upsell-звонком. Без видимости Asana-задач в CRM:

  • Менеджер звонит клиенту, не зная что онбординг ещё не завершён
  • Ops-команда не видит когда sales ставит follow-up (без двусторонней синхронизации)
  • Customer Success не знает, какие задачи из onboarding выполнены

При 20+ активных клиентах это становится системной проблемой координации.

Архитектура решения

Asana: задача выполнена (completed = true)
  -> Asana webhook -> POST your-server/webhooks/asana
  -> Ваш сервер:
     -> GET Asana /tasks/{gid}: получить название, проект, исполнитель
     -> Найти HubSpot Deal по кастомному полю (asana_project_id или deal_id)
     -> POST HubSpot /crm/v3/objects/notes
        {properties: {hs_note_body, hs_timestamp},
         associations: [{to: {id: dealId}, types: [{associationTypeId: 214}]}]}

Настройка Asana webhook

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

app = Flask(__name__)

ASANA_PAT        = os.environ["ASANA_ACCESS_TOKEN"]
ASANA_BASE       = "https://app.asana.com/api/1.0"
ASANA_HDR        = {"Authorization": f"Bearer {ASANA_PAT}",
                    "Content-Type": "application/json"}

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

# Хранить: Asana project_gid -> HubSpot deal_id
# В production - PostgreSQL или Redis
project_to_deal: dict = {}

def register_asana_webhook(project_gid: str,
                             callback_url: str, deal_id: str) -> str:
    r = requests.post(
        f"{ASANA_BASE}/webhooks",
        headers=ASANA_HDR,
        json={
            "data": {
                "resource": project_gid,
                "target":   callback_url,
                "filters": [
                    {"resource_type": "task", "action": "changed",
                     "fields": ["completed"]},
                    {"resource_type": "task", "action": "added"},
                ],
            }
        },
    )
    r.raise_for_status()
    webhook_id = r.json()["data"]["gid"]
    project_to_deal[project_gid] = deal_id
    return webhook_id

@app.route("/webhooks/asana", methods=["POST"])
def asana_webhook():
    # Handshake: при первом запросе Asana присылает X-Hook-Secret
    secret = request.headers.get("X-Hook-Secret")
    if secret:
        # Записать секрет для верификации последующих запросов
        # В production сохраните в env/DB
        return "", 200, {"X-Hook-Secret": secret}

    # Верификация HMAC (после первого handshake)
    hook_secret = os.environ.get("ASANA_HOOK_SECRET", "")
    if hook_secret:
        sig  = request.headers.get("X-Hook-Signature", "")
        body = request.get_data()
        expected = hmac.new(
            hook_secret.encode(), body, hashlib.sha256
        ).hexdigest()
        if not hmac.compare_digest(sig, expected):
            abort(401)

    events = request.json.get("events", [])
    for event in events:
        if event.get("type") != "task":
            continue
        if event.get("action") != "changed":
            continue

        task_gid    = event.get("resource", {}).get("gid", "")
        project_gid = event.get("parent", {}).get("gid", "")

        if not task_gid:
            continue

        # Получить детали задачи
        rt = requests.get(
            f"{ASANA_BASE}/tasks/{task_gid}",
            headers=ASANA_HDR,
            params={"opt_fields": "name,completed,assignee.name,due_on,notes"},
        )
        task = rt.json().get("data", {})
        if not task.get("completed"):
            continue  # нас интересует только завершение

        deal_id = project_to_deal.get(project_gid)
        if not deal_id:
            continue

        task_name    = task.get("name", "Task")
        assignee     = (task.get("assignee") or {}).get("name", "Unassigned")
        completed_at = event.get("created_at", "")

        note_body = (
            f"Asana task completed: {task_name}\n"
            f"Assignee: {assignee}\n"
            f"Project GID: {project_gid}"
        )
        create_hubspot_note(deal_id, note_body, completed_at)

    return jsonify({"status": "ok"}), 200

def create_hubspot_note(deal_id: str,
                         note_body: str, timestamp: str) -> str:
    from datetime import datetime
    ts_ms = int(
        datetime.fromisoformat(
            timestamp.replace("Z", "+00:00")
        ).timestamp() * 1000
    ) if timestamp else None

    payload = {
        "properties": {
            "hs_note_body": note_body,
            "hs_timestamp": str(ts_ms) if ts_ms else "",
        },
        "associations": [
            {
                "to": {"id": deal_id},
                "types": [
                    {
                        "associationCategory": "HUBSPOT_DEFINED",
                        "associationTypeId":   214,
                    }
                ],
            }
        ],
    }
    r = requests.post(
        f"{HS_BASE}/crm/v3/objects/notes",
        headers=HS_HDR,
        json=payload,
    )
    r.raise_for_status()
    return r.json()["id"]

Связь Asana project с HubSpot deal

Mapping project_gid -> deal_id нужно создавать при выигрыше сделки. Обычный flow:

# При HubSpot webhook deal.propertyChange (dealstage = closedwon):
def on_deal_won(deal_id: str):
    asana_project_gid = create_asana_project_for_deal(deal_id)
    register_asana_webhook(
        asana_project_gid,
        "https://your-server.com/webhooks/asana",
        deal_id,
    )
    # Сохранить mapping в БД
    db.save_mapping(asana_project_gid, deal_id)

Или: если Asana проекты создаются вручную - добавьте кастомное поле “HubSpot Deal ID” в Asana project и читайте его при обработке webhook-событий.

Реальный кейс

SaaS компания, 30 клиентов в онбординге одновременно. Asana - инструмент CS-команды (5-7 задач на клиента: setup, training, first value, NPS). Sales-менеджеры не могли видеть статус онбординга перед renewal-звонками. После интеграции: каждое завершение Asana-задачи появляется в HubSpot Deal Timeline. Менеджер заходит в сделку и видит полную картину без вопросов в Slack. Время подготовки к renewal-звонку сократилось с 15 до 3 минут.

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

B2B компании с разделением ролей sales / CS / ops, которые используют HubSpot как CRM и Asana как инструмент реализации. Особенно актуально для SaaS с онбордингом, консалтинговых компаний и агентств. Если у вас больше 10 активных клиентов в Asana - отсутствие видимости в CRM становится системной проблемой.

Другие HubSpot-антипаттерны: HubSpot + Harvest: time tracking не в Deal (logged hours), HubSpot + Loom: видео не в Deal Timeline.

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

Как Asana верифицирует webhook при первом запросе?

Первый запрос от Asana содержит заголовок X-Hook-Secret. Ваш сервер должен вернуть этот же секрет в заголовке ответа X-Hook-Secret. После этого Asana начинает подписывать запросы через HMAC-SHA256 - ваш сервер верифицирует подпись в X-Hook-Signature.

Почему associationTypeId: 214 для Note -> Deal?

214 - стандартный HubSpot ID для связи Note с Deal. Полный список: GET /crm/v4/associations/{fromObjectType}/{toObjectType}/labels с fromObjectType=notes, toObjectType=deals. Для Note -> Contact - 202, Note -> Company - 190.

Можно ли синхронизировать задачи в обратную сторону (HubSpot -> Asana)?

Да: HubSpot webhook deal.propertyChange при изменении нужного поля -> ваш сервер -> POST /tasks в Asana. Это двусторонняя синхронизация, которую нативная интеграция также не поддерживает.

Как найти HubSpot deal_id по email клиента в Asana?

В Asana нет прямой связи с HubSpot. Решение: хранить mapping в PostgreSQL (asana_project_gid -> hubspot_deal_id), который создаётся при создании проекта. Или добавить кастомное поле “HubSpot Deal ID” в Asana Project и читать его при обработке событий через GET /projects/{gid}?opt_fields=custom_fields.

Итог

HubSpot + Asana нативная интеграция - неполная:

  • Нативная: создаёт задачи, но не добавляет события в Deal Timeline
  • Правильный подход: Asana webhook -> HubSpot Note с associationTypeId: 214
  • Handshake: вернуть X-Hook-Secret в заголовке ответа
  • Хранить mapping project_gid -> deal_id в БД для связи событий
  • Результат: полная видимость Asana-задач в HubSpot Deal Timeline

Если ваша команда использует HubSpot + Asana и хочет настроить правильную двустороннюю видимость - опишите задачу команде Exceltic.dev.

Ещё статьи

Все →