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:
- Kommo -> Campaign Monitor: a deal stage change updates the subscriber’s tags in CM
- 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.