Beehiiv is a newsletter platform with a REST API for managing subscribers programmatically. For B2B companies that maintain their customer base in Kommo, the integration solves an obvious problem: a client bought - which means they should be receiving product updates, case studies, and nurture content. Instead of manually exporting a CSV and importing it into Beehiiv - one automatic subscription triggered by a stage change in the CRM.
Beehiiv API uses a Bearer token (API Key from Beehiiv Settings -> API). The key endpoint: POST /v2/publications/{publicationId}/subscriptions - add a subscriber with custom fields and UTM parameters. The Publication ID is taken from the URL of your Beehiiv account.
Beehiiv is a newsletter platform that has gained popularity as an alternative to Substack for B2B companies. Unlike Mailchimp and ActiveCampaign - it is not an email marketing tool, but a newsletter platform with built-in analytics, a referral program, and audience segmentation.
Why the Standard Approach Falls Short
The Zapier connector for Beehiiv adds a subscriber but does not pass custom fields (custom_fields) - and those are needed for segmentation: client type, company size, deal source. Without these fields, all new subscribers end up in one segment with no way to send relevant content.
Another problem: there is no feedback loop. When a client unsubscribes from Beehiiv, Kommo has no idea. The sales rep keeps referencing a newsletter the person no longer reads.
Architecture
Kommo: deal -> Closed Won (or another target stage)
-> Kommo webhook leads.status.changed
-> Your server
Your server:
-> Kommo API: fetch email, name, company of the contact
-> Beehiiv: POST /v2/publications/{pubId}/subscriptions
{email, utm_source: "kommo_crm", custom_fields: [...]}
-> Kommo: note "Subscribed to Beehiiv newsletter"
Beehiiv webhook (optional):
-> subscriptions.unsubscribed -> Kommo: note "Unsubscribed from newsletter"
Implementation: Subscribe on Deal Close
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 = ""
# Try to get company name
companies = lead.get("_embedded", {}).get("companies", [])
if companies:
company = companies[0].get("name", "")
if not email:
add_note(lead_id, "Beehiiv: email not found, subscription not created.")
continue
# Check if already subscribed
existing = check_subscription(email)
if existing and existing.get("status") == "active":
add_note(lead_id, f"Beehiiv: {email} is already subscribed (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} subscribed to newsletter. ID: {sub_id}"
)
return jsonify({"status": "ok"}), 200
Handling Unsubscribes
@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
# Find contact in Kommo by 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"]
# Add note to the contact
requests.post(
f"{KOMMO_BASE}/notes",
headers=KOMMO_HDR,
json=[{"entity_id": contact_id, "entity_type": "contacts",
"note_type": "common",
"params": {"text": f"Unsubscribed from Beehiiv newsletter. Sub ID: {sub_id}"}}],
)
return jsonify({"status": "ok"}), 200
Segmentation via custom_fields
Beehiiv custom fields allow you to build segments for sending: “only clients from deals over $10k”, “only from a specific pipeline”. Fields are created in advance in Beehiiv Settings -> Custom Fields.
Important: the name in the request must exactly match the field name in Beehiiv (case-sensitive). If the field does not exist, Beehiiv will return a 422 with an error description.
Beehiiv Webhook Verification
Beehiiv signs webhooks via HMAC-SHA256. The secret is available in 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)
# In the webhook handler:
# 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
Who This Is For
B2B SaaS and service companies that want a nurture sequence for new clients: onboarding content, case studies, product updates. Beehiiv is especially popular among companies building thought leadership through a newsletter - and wanting to automatically add clients from their CRM without manual imports.
Also relevant for companies re-monetizing their existing customer base: upsell campaigns via newsletter are far more effective than cold outreach.
Other email integrations: Kommo + MailerLite (email automation), Kommo + Postmark (transactional emails).
Frequently Asked Questions
Can subscribers be added directly to a specific Beehiiv segment?
Yes, via tags in the request body: "tags": ["new_customer", "enterprise"]. Tags must be created in Beehiiv in advance. Once a tag is added, the subscriber is automatically placed in the corresponding segment.
How do I unsubscribe a client from Kommo?
DELETE /v2/publications/{publicationId}/subscriptions/{subscriptionId}. The Subscription ID must be saved when the subscription is created (the id field in the response). We recommend storing it in a custom field on the Kommo contact.
Does reactivate_existing: true work for returning clients?
Yes. If the email already exists in Beehiiv with a status of inactive or unsubscribed, the reactivate_existing: true parameter will move it back to active. Without this parameter, attempting to add an existing email will return a 422 error.
How do I sync custom_fields when contact data is updated in Kommo?
Update the subscriber: PATCH /v2/publications/{pubId}/subscriptions/{subId} with a new set of custom_fields. The trigger is the Kommo webhook contacts.update. Relevant if the client type or company size changes in the CRM.
Summary
Kommo + Beehiiv - nurture subscriptions from the pipeline:
- Bearer token,
POST /v2/publications/{pubId}/subscriptions custom_fieldsfor segmentation,utm_source: "kommo_crm"for analyticsreactivate_existing: truefor returning clients- Webhook
subscriptions.unsubscribed-> note on the Kommo contact - Check
GET .../by_emailbefore subscribing to avoid duplicates
If your team wants to automatically add clients from Kommo to a Beehiiv newsletter - describe your requirements to the Exceltic.dev team.