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.