Kommo + Campaign Monitor: Email Campaigns Segmented by Pipeline Stage

Campaign Monitor is an email marketing platform with a straightforward API and reliable deliverability. Kommo is a CRM with a sales pipeline. Without integration, your mailing list lives separately from your pipeline: marketers don’t know which stage each subscriber is at and can’t segment campaigns by CRM status. With integration, moving a deal to a new stage automatically updates the subscriber’s tags in Campaign Monitor - making campaigns far more targeted.

The key difference between Campaign Monitor and Mailchimp in a B2B context: CM treats subscribers as a customer journey, supports trigger-based automations from tags, and integrates well into multi-CRM stacks. For companies targeting audiences outside the CIS region, CM is often preferred for its high deliverability to European mail systems.

Campaign Monitor API uses Basic Auth with the API key as the username and an arbitrary string as the password. All requests go to https://api.createsend.com/api/v3.3/.

Integration Directions

Two-way synchronization:

  1. Kommo -> Campaign Monitor: a deal stage change updates the subscriber’s tags in CM
  2. Campaign Monitor -> Kommo: email opens and clicks are logged as notes on the deal card
Kommo: deal -> stage "Proposal Sent"
  -> Find subscriber in CM by email
  -> Add tag "stage_proposal_sent"
  -> Remove tag from previous stage

Campaign Monitor: subscriber opened email
  -> POST /your-server/webhooks/cm {event: "Open", EmailAddress: "..."}
  -> Find deal in Kommo by email
  -> POST /api/v4/leads/{id}/notes {text: "Opened email: subject"}

Implementation: Kommo -> Campaign Monitor

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

app = Flask(__name__)

KOMMO_DOMAIN = os.environ["KOMMO_DOMAIN"]
KOMMO_TOKEN  = os.environ["KOMMO_TOKEN"]
CM_API_KEY   = os.environ["CM_API_KEY"]
CM_LIST_ID   = os.environ["CM_LIST_ID"]

KOMMO_BASE = f"https://{KOMMO_DOMAIN}/api/v4"
KOMMO_HDR  = {"Authorization": f"Bearer {KOMMO_TOKEN}"}

CM_BASE    = "https://api.createsend.com/api/v3.3"
CM_HDR     = {
    "Authorization": "Basic " + base64.b64encode(f"{CM_API_KEY}:x".encode()).decode(),
    "Content-Type": "application/json",
}

# Kommo stage -> Campaign Monitor tag mapping
STAGE_TAGS = {
    11111: "stage_new_lead",
    22222: "stage_qualified",
    33333: "stage_proposal_sent",
    44444: "stage_negotiation",
    142:   "stage_won",
    143:   "stage_lost",
}

@app.route("/webhooks/kommo", methods=["POST"])
def kommo_event():
    data = request.json or {}
    for lead in data.get("leads", {}).get("status", []):
        handle_stage_change(lead["id"], lead.get("status_id"))
    return "ok", 200

def get_contact_email(lead_id: int) -> str | None:
    r = requests.get(
        f"{KOMMO_BASE}/leads/{lead_id}",
        headers=KOMMO_HDR,
        params={"with": "contacts"},
    )
    if not r.ok:
        return None
    contacts = r.json().get("_embedded", {}).get("contacts", [])
    if not contacts:
        return None
    cr = requests.get(f"{KOMMO_BASE}/contacts/{contacts[0]['id']}", headers=KOMMO_HDR)
    if not cr.ok:
        return None
    for f in cr.json().get("custom_fields_values") or []:
        if f.get("field_code") == "EMAIL":
            vals = f.get("values", [])
            if vals:
                return str(vals[0]["value"])
    return None

def update_subscriber_tags(email: str, new_stage_id: int):
    # Get current subscriber
    r = requests.get(
        f"{CM_BASE}/subscribers/{CM_LIST_ID}.json",
        headers=CM_HDR,
        params={"email": email},
    )

    if r.status_code == 404:
        # Subscriber not found - create
        requests.post(
            f"{CM_BASE}/subscribers/{CM_LIST_ID}.json",
            headers=CM_HDR,
            json={
                "EmailAddress": email,
                "Resubscribe": True,
                "Tags": [STAGE_TAGS.get(new_stage_id, "stage_unknown")],
            },
        )
        return

    if not r.ok:
        return

    # Current subscriber tags
    current_tags = r.json().get("Tags", [])

    # Remove all stage_ tags, add new one
    stage_tag_values = list(STAGE_TAGS.values())
    clean_tags = [t for t in current_tags if t not in stage_tag_values]
    new_tag = STAGE_TAGS.get(new_stage_id)
    if new_tag:
        clean_tags.append(new_tag)

    # Update via /updateemail or recreate with new tags
    requests.put(
        f"{CM_BASE}/subscribers/{CM_LIST_ID}.json",
        headers=CM_HDR,
        params={"email": email},
        json={
            "EmailAddress": email,
            "Tags": clean_tags,
            "Resubscribe": False,
        },
    )

def handle_stage_change(lead_id: int, status_id: int | None):
    if status_id is None:
        return
    email = get_contact_email(lead_id)
    if not email:
        return
    update_subscriber_tags(email, status_id)

Implementation: Campaign Monitor -> Kommo

Campaign Monitor sends webhook events on email opens, clicks, and unsubscribes. Configure a Webhook in CM Dashboard > Transactional > Webhooks:

@app.route("/webhooks/cm", methods=["POST"])
def cm_event():
    # CM webhooks are not HMAC-signed - validate secret token in URL
    # Configure URL as /webhooks/cm?secret=YOUR_SECRET
    secret = request.args.get("secret", "")
    if secret != os.environ.get("CM_WEBHOOK_SECRET", ""):
        return "unauthorized", 401

    events = request.json or []
    if not isinstance(events, list):
        events = [events]

    for event in events:
        event_type = event.get("Type", "")
        email = event.get("EmailAddress", "")
        subject = event.get("Subject", "")

        if event_type in ("Open", "Click") and email:
            log_cm_event_to_kommo(email, event_type, subject)

    return "ok", 200

def find_lead_by_email(email: str) -> int | None:
    r = requests.get(
        f"{KOMMO_BASE}/contacts",
        headers=KOMMO_HDR,
        params={"query": email, "limit": 1},
    )
    if not r.ok:
        return None
    contacts = r.json().get("_embedded", {}).get("contacts", [])
    if not contacts:
        return None
    contact_id = contacts[0]["id"]
    lr = requests.get(f"{KOMMO_BASE}/contacts/{contact_id}/links", headers=KOMMO_HDR)
    if not lr.ok:
        return None
    links = lr.json().get("_embedded", {}).get("links", [])
    lead_links = [l for l in links if l.get("to_entity_type") == "leads"]
    return lead_links[0]["to_entity_id"] if lead_links else None

def log_cm_event_to_kommo(email: str, event_type: str, subject: str):
    lead_id = find_lead_by_email(email)
    if not lead_id:
        return
    label = "Opened email" if event_type == "Open" else "Clicked in email"
    requests.post(
        f"{KOMMO_BASE}/leads/{lead_id}/notes",
        headers=KOMMO_HDR,
        json=[{"note_type": "common", "params": {"text": f"Campaign Monitor: {label} - {subject}"}}],
    )

Hot Leads from Email Activity

Add logic: if a lead has opened 3+ emails in the past week - create a task for the account manager. This is implemented using a counter in Redis or PostgreSQL: increment the counter for the email address on each Open event. When the threshold is reached, create a task in Kommo.

Real-World Case

A company with 500 leads in Kommo and regular email campaigns in Campaign Monitor. Before integration: the marketer segmented the list manually once a week. After: tags update in real time whenever a stage changes. CTR for the “stage_negotiation” segment increased by 34% compared to unsegmented campaigns.

Who This Is For

Companies running active email marketing alongside a parallel CRM sales pipeline. Especially relevant if you have a long deal cycle (30+ days) - nurture emails during that period should match the current negotiation stage.

Related integrations: Kommo + ClickUp for tasks triggered by stage changes, custom integrations in Kommo CRM for an overview of architectural approaches.

Frequently Asked Questions

How does Campaign Monitor handle the same email address across different lists?

Each list in CM is independent. A single subscriber can exist in multiple lists with different tags. For Kommo integration, the recommended approach is one master list “CRM Leads” - all CRM contacts in one place, tagged by stage.

Does Campaign Monitor support double opt-in when creating a subscriber via API?

Yes, but when creating via API with "ConsentToTrack": "Yes" and "Resubscribe": true you can add subscribers without double opt-in. Make sure you have a lawful basis for sending under GDPR/CAN-SPAM.

How should unsubscribes in Campaign Monitor be handled - remove from Kommo?

On an Unsubscribe event from the CM webhook, add an “unsubscribed” tag to a custom field on the contact in Kommo. Do not delete the contact - preserve the history. A sales manager can reach out through a different channel.

Summary

Kommo + Campaign Monitor integration:

  • Kommo webhook (deal status) -> update subscriber tags in CM
  • Campaign Monitor webhook (Open/Click) -> note on the deal card
  • Real-time campaign segmentation by pipeline stage
  • Basic Auth: base64(api_key:x) in the Authorization header

If you want to set up email campaigns with precise pipeline-based segmentation - reach out to Exceltic.dev. We’ll build the integration tailored to your stages and CM templates.

More articles

All →