Kommo + Beehiiv: Auto-Subscribe to Newsletter When a Deal Is Won

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_fields for segmentation, utm_source: "kommo_crm" for analytics
  • reactivate_existing: true for returning clients
  • Webhook subscriptions.unsubscribed -> note on the Kommo contact
  • Check GET .../by_email before 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.

More articles

All →