Kommo + Lemlist: Cold Email Campaigns from the Pipeline Without Contact Duplicates

Kommo + Lemlist: Cold Email Campaigns from the Pipeline Without Contact Duplicates

When Kommo and Lemlist are integrated, a lead that reaches the right pipeline stage is automatically added to a Lemlist campaign. A reply or link click updates the status in Kommo. An unsubscribe removes the contact from all active campaigns. SDRs stop exporting CSVs and stop checking manually who has already been contacted.

Lemlist is a platform for personalized cold email campaigns with automated follow-up sequences, personalized images, and multi-channel outreach (email + LinkedIn). It is popular among SDR teams and agencies as a replacement for mass email blasts. The problem is that Lemlist and Kommo live in parallel universes: without integration, an SDR adds contacts to Lemlist manually and then manually copies replies back into the CRM.

There is no native integration between Kommo and Lemlist. According to Lemlist data, the average reply rate in B2B cold email is 3-8%, which means an SDR works through hundreds of contacts before getting a handful of replies. At that volume, manually adding leads into sequences becomes the primary time drain. In Kommo projects we see that leads are added to Lemlist campaigns in batches every few days - during that window some contacts move through multiple pipeline stages or become irrelevant entirely. This article covers how a custom integration ties the Kommo pipeline stage transition to a personalized Lemlist sequence enrollment in real time.

Why the native integration does not work

Lemlist offers a Zapier integration, but it only handles the simplest case: “when a contact is added in Lemlist - create a deal in Kommo.” The reverse direction (from Kommo to Lemlist) works in a limited way.

The main problem is deduplication. If the same contact exists in multiple Kommo deals, Zapier will create them in Lemlist multiple times, and the person will receive the same email twice with different context. This destroys domain reputation.

The second problem is unsubscribe management. When a contact clicks “Unsubscribe” in Lemlist, that should block all future campaigns to that email. Kommo does not update automatically - the manager has no visibility that the contact opted out and may manually launch the next campaign.

The third problem is variability. Lemlist allows personalizing emails through variables (name, company, a case study relevant to their industry). That data lives in Kommo, but Zapier cannot dynamically collect it from custom fields and pass it in the correct format.

What gets built - solution architecture

Kommo (deal moved to "Outreach" stage)
        |
        v
  Orchestrator server
  - check: is the contact already in an active campaign?
  - check: has the contact unsubscribed?
  - assemble personalization variables from deal fields
        |
        v
  Lemlist API (add to campaign with variables)
        |
        v
  Kommo: update lemlist_campaign_id field

  ---

Lemlist (event: reply / click / unsubscribe)
        |
        v
  Lemlist Webhook -> Your server
        |
        v
  Kommo API:
  - if reply -> add note + task "Reply"
  - if click -> add note
  - if unsubscribe -> update field + tag "unsubscribed"

The key component is the active campaign registry. Before adding a contact to Lemlist, the server checks via the Lemlist API whether that email is already in another active campaign. This prevents duplicates when multiple SDRs work with overlapping segments.

Technical details

Lemlist API v2 uses an API key in the Authorization: Bearer <key> header. The key is generated in Settings -> Integrations -> API. Webhooks are configured in Settings -> Integrations -> Webhooks and support the events: emailSent, emailOpened, emailClicked, emailReplied, emailBounced, emailUnsubscribed.

import requests
from flask import Flask, request, jsonify

app = Flask(__name__)

LEMLIST_API_BASE = "https://api.lemlist.com/api"
LEMLIST_API_KEY = "your_lemlist_api_key"

def check_contact_in_campaign(email: str) -> bool:
    """Check whether the contact is in an active Lemlist campaign"""
    headers = {"Authorization": f"Bearer {LEMLIST_API_KEY}"}
    resp = requests.get(
        f"{LEMLIST_API_BASE}/leads/{email}",
        headers=headers
    )
    if resp.status_code == 404:
        return False
    lead_data = resp.json()
    # Check campaign status
    for campaign in lead_data.get("campaigns", []):
        if campaign.get("status") in ["active", "paused"]:
            return True
    return False

def add_lead_to_campaign(campaign_id: str, lead_data: dict) -> dict:
    """Add a lead to a Lemlist campaign with personalization from Kommo"""
    headers = {"Authorization": f"Bearer {LEMLIST_API_KEY}"}
    payload = {
        "firstName": lead_data["first_name"],
        "lastName": lead_data["last_name"],
        "email": lead_data["email"],
        "companyName": lead_data.get("company", ""),
        "icebreaker": lead_data.get("personalization_note", ""),
        "industry": lead_data.get("industry", ""),
        "dealSize": str(lead_data.get("deal_amount", "")),
        "kommoLeadId": str(lead_data["kommo_lead_id"])
    }
    resp = requests.post(
        f"{LEMLIST_API_BASE}/campaigns/{campaign_id}/leads/{lead_data['email']}",
        json=payload,
        headers=headers
    )
    resp.raise_for_status()
    return resp.json()

@app.route("/kommo/webhook", methods=["POST"])
def kommo_webhook():
    data = request.json
    for lead_update in data.get("leads", {}).get("status", []):
        lead_id = lead_update["id"]
        new_stage = lead_update["status_id"]
        if new_stage == OUTREACH_STAGE_ID:
            handle_outreach_stage(lead_id)
    return jsonify({"ok": True})

def handle_outreach_stage(lead_id: int):
    lead = kommo_api.get_lead(lead_id)
    contact = kommo_api.get_lead_contact(lead_id)
    email = contact.get("email")
    
    if not email:
        kommo_api.add_note(lead_id, "Lemlist: no email on contact, skipping")
        return
    
    # Check for unsubscribe
    if contact.get("custom_fields", {}).get("email_unsubscribed"):
        kommo_api.add_note(lead_id, "Lemlist: contact has unsubscribed, skipping")
        return
    
    # Check for duplicate
    if check_contact_in_campaign(email):
        kommo_api.add_note(lead_id, f"Lemlist: {email} is already in an active campaign")
        return
    
    # Determine campaign by deal type
    campaign_id = get_campaign_for_deal(lead)
    
    lead_data = {
        "first_name": contact.get("name", "").split()[0],
        "last_name": " ".join(contact.get("name", "").split()[1:]),
        "email": email,
        "company": lead.get("company_name", ""),
        "deal_amount": lead.get("price", 0),
        "industry": lead.get("custom_fields", {}).get("industry", ""),
        "personalization_note": lead.get("custom_fields", {}).get("icebreaker", ""),
        "kommo_lead_id": lead_id
    }
    
    result = add_lead_to_campaign(campaign_id, lead_data)
    kommo_api.update_lead(lead_id, {"lemlist_campaign_id": campaign_id})
    kommo_api.add_note(lead_id, f"Added to Lemlist campaign {campaign_id}")

@app.route("/lemlist/webhook", methods=["POST"])
def lemlist_webhook():
    data = request.json
    event = data.get("type")
    email = data.get("email")
    kommo_lead_id = data.get("metaData", {}).get("kommoLeadId")
    
    if not kommo_lead_id:
        # Attempt to find by email
        contact = kommo_api.find_contact_by_email(email)
        if contact:
            deals = kommo_api.get_contact_deals(contact["id"])
            kommo_lead_id = deals[0]["id"] if deals else None
    
    if not kommo_lead_id:
        return jsonify({"ok": True})
    
    if event == "emailReplied":
        kommo_api.add_note(kommo_lead_id, f"Lemlist: reply received from {email}")
        kommo_api.create_task(
            kommo_lead_id,
            text=f"Reply to email from {email}",
            deadline_hours=4
        )
    elif event == "emailClicked":
        kommo_api.add_note(kommo_lead_id, f"Lemlist: {email} clicked a link")
    elif event == "emailUnsubscribed":
        kommo_api.update_contact_field(email, "email_unsubscribed", True)
        kommo_api.add_tag_to_contact(email, "unsubscribed")
        kommo_api.add_note(kommo_lead_id, f"Lemlist: {email} unsubscribed")
    elif event == "emailBounced":
        kommo_api.add_note(kommo_lead_id, f"Lemlist: email {email} is unreachable (bounce)")
    
    return jsonify({"ok": True})

Step-by-step implementation

Step 1. Create campaigns in Lemlist

Prepare campaigns for different segments: by industry, company size, lead source. In each campaign, configure variables: {{firstName}}, {{companyName}}, {{icebreaker}}. Record the Campaign ID of each campaign - it is needed for mapping.

Step 2. Configure custom fields in Kommo

Add fields: lemlist_campaign_id (text), email_unsubscribed (checkbox), icebreaker (text - a field for the personalized opening line). The icebreaker field is filled by the SDR before moving the lead to the “Outreach” stage.

Step 3. Configure campaign mapping

Define the campaign selection logic: for example, if industry = SaaS -> campaign A, if deal_amount > 10000 -> campaign B. This is implemented in server logic, not in Zapier.

Step 4. Create a Kommo webhook

Subscribe to the leads.status event. When a deal transitions to the “Outreach” stage, the server launches the Lemlist enrollment process.

Step 5. Configure the Lemlist webhook

In Lemlist Settings -> Integrations -> Webhooks, add the server URL and subscribe to events: emailReplied, emailClicked, emailUnsubscribed, emailBounced.

Real case with numbers

Agency segment: B2B agency from Warsaw, 4 SDRs, 150-200 new leads per month. Before the integration the process looked like this:

  • SDR qualified a lead in Kommo
  • Exported a CSV from the relevant stage
  • Imported it into Lemlist manually
  • After receiving a reply, manually created a note in Kommo
  • Checked twice a week who replied and who unsubscribed

Time spent maintaining the process: 3-4 hours per week per SDR. With 4 SDRs - 12-16 hours of administrative work per week.

Duplicate rate: 1-2 contacts per week received identical emails from different campaigns due to human error during de-dup checks.

After the Kommo + Lemlist integration via Exceltic.dev:

  • Campaign enrollment: automatic on stage change
  • Duplicates: 0 (API-level check)
  • Replies in Kommo: appear automatically as notes
  • Administrative time: reduced to 30 minutes per week for the entire team

For more on Kommo CRM features for outreach teams, see the platform overview.

Who this is for

The Kommo + Lemlist integration is relevant for:

  • SDR teams that combine outbound prospecting with pipeline management in Kommo
  • Agencies running multiple campaigns in parallel for different segments
  • Companies with 50+ new leads per month where manual import into outreach tools has become a bottleneck
  • Teams with compliance requirements for unsubscribe management (GDPR, CAN-SPAM)

If you primarily work with inbound leads from forms and ads, Lemlist may be overkill. For warming a database via email, Kommo + ActiveCampaign or Brevo is sufficient.

Frequently asked questions

Does Lemlist support an API for adding leads to a campaign?

Yes. Lemlist API v2 supports adding leads to a campaign, removing them, retrieving statuses, and configuring webhooks. The API key is generated in Settings -> Integrations. Documentation is available at developer.lemlist.com.

How does the integration handle GDPR unsubscribe requirements?

When Lemlist sends an emailUnsubscribed event, the integration performs three actions: sets a flag in the Kommo contact’s custom field, adds the “unsubscribed” tag, and records a note with the unsubscribe date. Before adding any contact to a new campaign, the server checks this flag and blocks enrollment. This complies with GDPR opt-out requirements.

Can different campaigns run for different deal types?

Yes. The campaign selection logic is configured on the server side: depending on Kommo custom fields (industry, company size, lead source), the appropriate campaign_id is selected. The mapping logic can be as complex as needed.

What happens when a Lemlist campaign ends without a reply?

Lemlist sends a leadExhausted event when all sequence steps are completed and no reply was received. The integration updates a field in Kommo (“Outreach completed, no reply”) and can automatically move the deal to the next pipeline stage or create a task for manual follow-up.

How long does integration development take?

The basic version (campaign enrollment + reply/unsubscribe handling) takes 2-3 weeks. Complex campaign mapping logic, multi-channel outreach (email + LinkedIn), and integration with additional systems - 4-5 weeks. More on custom Kommo integrations for outreach automation is covered on the site.


If your SDR team works in Kommo and uses Lemlist for outreach - describe your requirements to the Exceltic.dev team. We will design the architecture for your process and estimate the scope.

More articles

All →