Kommo + Beehiiv: автоподписка на newsletter при закрытии сделки

Beehiiv - newsletter-платформа с REST API для управления подписчиками программно. Для B2B компаний, которые ведут клиентскую базу в Kommo, интеграция решает очевидную задачу: клиент купил - значит, он должен получать продуктовые обновления, кейсы и nurture-контент. Вместо ручного экспорта CSV и импорта в Beehiiv - одна автоматическая подписка при смене этапа в CRM.

Beehiiv API использует Bearer token (API Key из Beehiiv Settings -> API). Ключевой эндпоинт: POST /v2/publications/{publicationId}/subscriptions - добавить подписчика с кастомными полями и UTM-параметрами. Publication ID берётся из URL вашего Beehiiv-аккаунта.

Beehiiv - newsletter-платформа, которая набрала популярность как альтернатива Substack для B2B-компаний. В отличие от Mailchimp и ActiveCampaign - не email-маркетинг инструмент, а именно newsletter с встроенной аналитикой, referral-программой и сегментацией аудитории.

Почему стандартный подход неполный

Zapier-коннектор Beehiiv добавляет подписчика, но не передаёт кастомные поля (custom_fields) - а они нужны для сегментации: тип клиента, размер компании, источник сделки. Без этих полей все новые подписчики попадают в один сегмент без возможности отправить релевантный контент.

Другая проблема: нет обратной связи. Когда клиент отписывается от Beehiiv, Kommo об этом не знает. Менеджер продолжает ссылаться на newsletter, который человек уже не читает.

Архитектура

Kommo: сделка -> Closed Won (или другой целевой этап)
  -> Kommo webhook leads.status.changed
  -> Ваш сервер

Ваш сервер:
  -> Kommo API: получить email, имя, компанию контакта
  -> Beehiiv: POST /v2/publications/{pubId}/subscriptions
     {email, utm_source: "kommo_crm", custom_fields: [...]}
  -> Kommo: note "Подписан на Beehiiv newsletter"

Beehiiv webhook (опционально):
  -> subscriptions.unsubscribed -> Kommo: note "Отписался от newsletter"

Реализация: подписка при закрытии сделки

import requests, os
from flask import Flask, request, jsonify

app = Flask(__name__)

BEEHIIV_KEY    = os.environ["BEEHIIV_API_KEY"]
BEEHIIV_PUB    = os.environ["BEEHIIV_PUBLICATION_ID"]  # pub_XXXXXXXXXXXXXXXX
BEEHIIV_BASE   = "https://api.beehiiv.com/v2"
BEEHIIV_HDR    = {"Authorization": f"Bearer {BEEHIIV_KEY}",
                  "Content-Type": "application/json"}

KOMMO_SUBDOMAIN  = os.environ["KOMMO_SUBDOMAIN"]
KOMMO_TOKEN      = os.environ["KOMMO_ACCESS_TOKEN"]
CLOSED_WON_ID    = int(os.environ["KOMMO_CLOSED_WON_ID"])

KOMMO_BASE = f"https://{KOMMO_SUBDOMAIN}.kommo.com/api/v4"
KOMMO_HDR  = {"Authorization": f"Bearer {KOMMO_TOKEN}",
              "Content-Type": "application/json"}

def get_lead_contact(lead_id: int) -> tuple:
    r = requests.get(
        f"{KOMMO_BASE}/leads/{lead_id}",
        headers=KOMMO_HDR,
        params={"with": "contacts,custom_fields_values"},
    )
    lead = r.json()
    contacts = lead.get("_embedded", {}).get("contacts", [])
    contact = {}
    if contacts:
        rc = requests.get(
            f"{KOMMO_BASE}/contacts/{contacts[0]['id']}",
            headers=KOMMO_HDR,
            params={"with": "custom_fields_values"},
        )
        contact = rc.json()
    return lead, contact

def get_cf(entity: dict, code: str) -> str:
    for cf in entity.get("custom_fields_values", []) or []:
        if cf.get("field_code") == code:
            vals = cf.get("values", [])
            return vals[0].get("value", "") if vals else ""
    return ""

def subscribe_to_beehiiv(email: str, name: str,
                          company: str, lead_id: int) -> dict:
    r = requests.post(
        f"{BEEHIIV_BASE}/publications/{BEEHIIV_PUB}/subscriptions",
        headers=BEEHIIV_HDR,
        json={
            "email":               email,
            "reactivate_existing": True,
            "send_welcome_email":  False,
            "utm_source":          "kommo_crm",
            "utm_medium":          "crm_integration",
            "utm_campaign":        "closed_won",
            "custom_fields": [
                {"name": "kommo_deal_id",  "value": str(lead_id)},
                {"name": "company_name",   "value": company},
                {"name": "full_name",      "value": name},
                {"name": "customer_type",  "value": "paid"},
            ],
        },
    )
    r.raise_for_status()
    return r.json()

def check_subscription(email: str) -> dict | None:
    r = requests.get(
        f"{BEEHIIV_BASE}/publications/{BEEHIIV_PUB}/subscriptions/by_email",
        headers=BEEHIIV_HDR,
        params={"email": email},
    )
    if r.status_code == 404:
        return None
    r.raise_for_status()
    return r.json().get("data")

def add_note(lead_id: int, text: str):
    requests.post(
        f"{KOMMO_BASE}/notes",
        headers=KOMMO_HDR,
        json=[{"entity_id": lead_id, "entity_type": "leads",
               "note_type": "common", "params": {"text": text}}],
    )

@app.route("/webhooks/kommo", methods=["POST"])
def kommo_webhook():
    data = request.json or {}
    for lead_data in data.get("leads", {}).get("status", []):
        lead_id    = lead_data.get("id")
        new_status = lead_data.get("status_id")
        if new_status != CLOSED_WON_ID:
            continue

        lead, contact = get_lead_contact(lead_id)
        email   = get_cf(contact, "EMAIL")
        name    = contact.get("name", "")
        company = ""

        # Попробовать получить компанию
        companies = lead.get("_embedded", {}).get("companies", [])
        if companies:
            company = companies[0].get("name", "")

        if not email:
            add_note(lead_id, "Beehiiv: email не найден, подписка не создана.")
            continue

        # Проверить, не подписан ли уже
        existing = check_subscription(email)
        if existing and existing.get("status") == "active":
            add_note(lead_id, f"Beehiiv: {email} уже подписан (active).")
            continue

        result = subscribe_to_beehiiv(email, name, company, lead_id)
        sub_id = result.get("data", {}).get("id", "")
        add_note(
            lead_id,
            f"Beehiiv: {email} подписан на newsletter. ID: {sub_id}"
        )

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

Обработка отписки

@app.route("/webhooks/beehiiv", methods=["POST"])
def beehiiv_webhook():
    event = request.json or {}
    if event.get("type") != "subscriptions.unsubscribed":
        return jsonify({"status": "ignored"}), 200

    data  = event.get("data", {})
    email = data.get("email", "")
    sub_id = data.get("id", "")

    if not email:
        return jsonify({"status": "no_email"}), 200

    # Найти контакт в Kommo по email
    r = requests.get(
        f"{KOMMO_BASE}/contacts",
        headers=KOMMO_HDR,
        params={"query": email},
    )
    contacts = r.json().get("_embedded", {}).get("contacts", [])
    if not contacts:
        return jsonify({"status": "contact_not_found"}), 200

    contact_id = contacts[0]["id"]
    # Добавить note к контакту
    requests.post(
        f"{KOMMO_BASE}/notes",
        headers=KOMMO_HDR,
        json=[{"entity_id": contact_id, "entity_type": "contacts",
               "note_type": "common",
               "params": {"text": f"Отписался от Beehiiv newsletter. Sub ID: {sub_id}"}}],
    )
    return jsonify({"status": "ok"}), 200

Сегментация через custom_fields

Beehiiv custom fields позволяют строить сегменты для отправки: “только клиенты из сделок с суммой >$10k”, “только из определённой воронки”. Поля создаются заранее в Beehiiv Settings -> Custom Fields.

Важно: в запросе name должно точно совпадать с именем поля в Beehiiv (регистр имеет значение). Если поле не существует - Beehiiv вернёт 422 с описанием ошибки.

Верификация webhook Beehiiv

Beehiiv подписывает webhook через HMAC-SHA256. Секрет доступен в Settings -> Webhooks:

import hmac, hashlib

def verify_beehiiv_signature(secret: str, body: bytes,
                              signature: str) -> bool:
    expected = hmac.new(
        secret.encode(), body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(f"sha256={expected}", signature)

# В обработчике webhook:
# sig = request.headers.get("X-Beehiiv-Signature", "")
# if not verify_beehiiv_signature(os.environ["BEEHIIV_WEBHOOK_SECRET"],
#                                  request.get_data(), sig):
#     return jsonify({"error": "invalid signature"}), 401

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

B2B SaaS и сервисные компании, которые хотят nurture-последовательность для новых клиентов: onboarding-контент, кейсы, product updates. Beehiiv особенно популярен среди компаний, которые строят thought leadership через newsletter - и хотят автоматически добавлять клиентов из CRM без ручного импорта.

Также актуально для компаний, которые повторно монетизируют существующую клиентскую базу: upsell-кампании через newsletter намного эффективнее холодных писем.

Другие email-интеграции: Kommo + MailerLite (email automation), Kommo + Postmark (транзакционные письма).

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

Можно ли добавлять подписчиков сразу в определённый сегмент Beehiiv?

Да, через tags в теле запроса: "tags": ["new_customer", "enterprise"]. Теги нужно создать заранее в Beehiiv. После добавления тега подписчик попадает в соответствующий сегмент автоматически.

Как отписать клиента из Kommo?

DELETE /v2/publications/{publicationId}/subscriptions/{subscriptionId}. Subscription ID нужно сохранить при создании подписки (поле id в ответе). Рекомендуем хранить в кастомном поле Kommo-контакта.

Работает ли reactivate_existing: true для повторных клиентов?

Да. Если email уже есть в базе Beehiiv со статусом inactive или unsubscribed - параметр reactivate_existing: true переведёт его обратно в active. Без этого параметра попытка добавить существующий email вернёт ошибку 422.

Как синхронизировать custom_fields при обновлении данных в Kommo?

Обновление подписчика: PATCH /v2/publications/{pubId}/subscriptions/{subId} с новым набором custom_fields. Триггер - Kommo webhook contacts.update. Актуально если тип клиента или размер компании меняется в CRM.

Итог

Kommo + Beehiiv - nurture-подписки из воронки:

  • Bearer token, POST /v2/publications/{pubId}/subscriptions
  • custom_fields для сегментации, utm_source: "kommo_crm" для аналитики
  • reactivate_existing: true для повторных клиентов
  • Webhook subscriptions.unsubscribed -> note в Kommo-контакте
  • Проверка GET .../by_email перед подпиской чтобы избежать дублей

Если ваша команда хочет автоматически добавлять клиентов из Kommo в Beehiiv newsletter - опишите задачу команде Exceltic.dev.

Ещё статьи

Все →