Kommo + Customer.io: Email Automation Driven by Pipeline Data

Customer.io is an email automation platform built around data-driven scenarios: campaign branching by profile attributes, behavioral triggers, and send-level A/B tests. In B2B sales, this means you can send different email sequences based on a lead’s industry, deal size, or pipeline stage in Kommo. Without an integration, CRM data and Customer.io data exist in isolation.

The typical problem: a lead moves to the “Proposal Sent” stage in Kommo - and a nurture sequence in Customer.io should start automatically. Or: a contact opens three emails in a row - that is a buying signal, and a call task should appear in Kommo. Without a bidirectional integration, both scenarios require manual intervention.

This article covers the Kommo - Customer.io bidirectional setup: pipeline events trigger email campaigns, and Customer.io engagement data flows back into the deal card.

Why There Is No Native Integration

Customer.io has no ready-made widget for Kommo. Direct integrations in Customer.io exist for Segment, Twilio, Salesforce, and a handful of others - Kommo is not on that list. Zapier supports Customer.io but only in one direction (Zapier -> Customer.io) and does not pass the full set of deal attributes needed for data-driven branching.

Customer.io is a platform for sending transactional and marketing email/push/SMS messages based on user events and attributes. It differs from Mailchimp/SendGrid in its emphasis on behavioral segmentation and branching workflows.

Bidirectional Integration Architecture

Direction 1 - Kommo -> Customer.io:

  • Kommo webhook (stage change / new lead) -> update Customer.io profile
  • Deal attributes (size, industry, stage) -> Customer.io person attributes
  • Stage change -> Customer.io custom event -> Campaign trigger

Direction 2 - Customer.io -> Kommo:

  • Customer.io webhook (email opened, clicked, unsubscribed) -> note in Kommo
  • Multiple opens in a row -> task for the manager to call

Implementation: Kommo -> Customer.io

Step 1 - subscribe to the Kommo webhook:

In Kommo, configure a webhook for the following events: deal status change, new lead added. Go to Kommo Admin -> Settings -> Webhooks.

from flask import Flask, request
import requests, base64

app = Flask(__name__)

CIO_SITE_ID  = "your_site_id"     # from Customer.io Account -> API Credentials
CIO_API_KEY  = "your_api_key"
CIO_TRACK_BASE = "https://track.customer.io/api/v1"

KOMMO_DOMAIN = "yourdomain.kommo.com"
KOMMO_TOKEN  = "your_kommo_token"
KOMMO_BASE   = f"https://{KOMMO_DOMAIN}/api/v4"

def cio_headers():
    creds = base64.b64encode(f"{CIO_SITE_ID}:{CIO_API_KEY}".encode()).decode()
    return {
        "Authorization": f"Basic {creds}",
        "Content-Type":  "application/json"
    }

@app.route("/kommo/webhook", methods=["POST"])
def kommo_webhook():
    data = request.json
    # Kommo sends data in the format {leads: {status_changed: [...]}}
    for event_type, leads in data.items():
        for entity_type, items in leads.items():
            for item in items:
                if entity_type == "status_changed":
                    on_lead_status_changed(item)
                elif entity_type == "add":
                    on_lead_added(item)
    return "ok", 200

Step 2 - push Kommo data to Customer.io:

def get_kommo_lead_full(lead_id: int) -> dict:
    hs = requests.Session()
    hs.headers.update({
        "Authorization": f"Bearer {KOMMO_TOKEN}",
        "Content-Type":  "application/json"
    })
    r = hs.get(f"{KOMMO_BASE}/leads/{lead_id}",
               params={"with": "contacts,custom_fields"})
    lead = r.json()

    # Find the contact's email
    contacts = lead.get("_embedded", {}).get("contacts", [])
    email = ""
    for c in contacts:
        for field in c.get("custom_fields_values") or []:
            if field.get("field_type") == "EMAIL":
                vals = field.get("values", [])
                if vals:
                    email = vals[0].get("value", "")
                    break

    return {"id": lead_id, "email": email, "lead": lead}

def on_lead_status_changed(item: dict):
    lead_id   = item.get("id")
    status_id = item.get("status_id")
    pipeline  = item.get("pipeline_id")

    ctx = get_kommo_lead_full(lead_id)
    if not ctx["email"]:
        return

    lead  = ctx["lead"]
    email = ctx["email"]

    # Stage ID -> human-readable name map
    stage_map = {
        1: "new", 2: "in_contact", 3: "proposal_sent",
        4: "negotiation", 142: "won", 143: "lost"
    }
    stage_name = stage_map.get(status_id, f"stage_{status_id}")

    # Update attributes in Customer.io
    cio = requests.Session()
    cio.headers.update(cio_headers())

    cio.put(f"{CIO_TRACK_BASE}/customers/{email}", json={
        "kommo_lead_id":    str(lead_id),
        "kommo_stage":      stage_name,
        "kommo_stage_id":   status_id,
        "kommo_pipeline":   pipeline,
        "deal_value":       lead.get("price", 0),
        "responsible_name": lead.get("responsible_user_id"),
    })

    # Send event to trigger a Campaign
    cio.post(f"{CIO_TRACK_BASE}/customers/{email}/events", json={
        "name": "kommo_stage_changed",
        "data": {
            "stage":      stage_name,
            "stage_id":   status_id,
            "lead_id":    lead_id,
            "deal_value": lead.get("price", 0),
        }
    })

def on_lead_added(item: dict):
    ctx = get_kommo_lead_full(item.get("id"))
    if not ctx["email"]:
        return

    lead  = ctx["lead"]
    email = ctx["email"]

    cio = requests.Session()
    cio.headers.update(cio_headers())

    # Identify the new user in Customer.io
    cio.put(f"{CIO_TRACK_BASE}/customers/{email}", json={
        "kommo_lead_id": str(lead.get("id")),
        "kommo_stage":   "new",
        "deal_value":    lead.get("price", 0),
        "created_at":    lead.get("created_at"),
    })

    # Event for the welcome sequence
    cio.post(f"{CIO_TRACK_BASE}/customers/{email}/events", json={
        "name": "kommo_lead_created",
        "data": {"lead_id": lead.get("id")}
    })

Implementation: Customer.io -> Kommo

Step 3 - handle email events from Customer.io:

In Customer.io, configure a Reporting Webhook: Workspace -> Settings -> Reporting Webhooks. Subscribe to email_opened, email_clicked, email_unsubscribed.

@app.route("/customerio/webhook", methods=["POST"])
def cio_webhook():
    # Customer.io signs the webhook via X-CIO-Signature (HMAC-SHA256)
    event_type  = request.json.get("metric", "")
    customer_id = request.json.get("customer_id", "")  # this is the email
    data        = request.json.get("data", {})

    if event_type in ("email_opened", "email_clicked"):
        on_email_engagement(customer_id, event_type, data)
    elif event_type == "email_unsubscribed":
        on_email_unsubscribed(customer_id, data)

    return "ok", 200

def find_kommo_lead_by_email(email: str) -> int | None:
    hs = requests.Session()
    hs.headers.update({
        "Authorization": f"Bearer {KOMMO_TOKEN}",
        "Content-Type":  "application/json"
    })
    r = hs.get(f"{KOMMO_BASE}/contacts", params={"query": email, "limit": 1})
    contacts = r.json().get("_embedded", {}).get("contacts", [])
    if not contacts:
        return None
    contact_id = contacts[0]["id"]
    r2 = hs.get(f"{KOMMO_BASE}/contacts/{contact_id}/links")
    links = r2.json().get("_embedded", {}).get("links", [])
    for l in links:
        if l.get("to_entity_type") == "leads":
            return l["to_entity_id"]
    return None

# Open counter for the hot-lead trigger
_open_counts: dict = {}

def on_email_engagement(email: str, event_type: str, data: dict):
    lead_id = find_kommo_lead_by_email(email)
    if not lead_id:
        return

    event_label = {"email_opened": "opened email", "email_clicked": "clicked a link"}
    subject     = data.get("subject", "")

    hs = requests.Session()
    hs.headers.update({
        "Authorization": f"Bearer {KOMMO_TOKEN}",
        "Content-Type":  "application/json"
    })
    hs.post(f"{KOMMO_BASE}/leads/notes", json=[{
        "entity_id": lead_id,
        "note_type":  "common",
        "params":     {"text": f"Customer.io: {email} {event_label[event_type]}. Subject: {subject}"}
    }])

    # Hot-lead logic: 3+ opens = time to call
    key = f"{email}:opens"
    _open_counts[key] = _open_counts.get(key, 0) + 1

    if _open_counts[key] >= 3:
        import time
        hs.post(f"{KOMMO_BASE}/tasks", json=[{
            "task_type_id": 1,
            "entity_type":  "leads",
            "entity_id":    lead_id,
            "text":         f"{email} opened emails 3 times - high interest. Call today.",
            "complete_till": int(time.time()) + 28800,  # 8 hours
        }])
        _open_counts[key] = 0  # reset

In production, replace _open_counts with a Redis counter with a 48-hour TTL - this prevents stale data from accumulating.

Real-World Case

B2B SaaS: 200 inbound leads per month, 4-week sales cycle. Before the integration: Customer.io campaigns were launched manually via a weekly CSV export from Kommo. 20-30% of leads dropped out of nurture sequences due to sync delays.

After the bidirectional integration:

  • Every new lead in Kommo is automatically identified in Customer.io
  • A stage change triggers the right Campaign within a second
  • “Hot” leads (3+ opens) automatically generate a call task for the manager
  • 0 manual CSV exports

Who This Is For

B2B teams that combine a sales pipeline in Kommo with marketing automation through Customer.io. It is especially effective for inbound models where leads go through email nurturing in parallel with manager outreach.

A similar integration for a different email platform is described in Kommo + Encharge. If you use SendGrid for transactional email, the architecture is different - see Kommo + SendGrid.

Frequently Asked Questions

How do I branch a Customer.io Campaign by deal size from Kommo?

In the Customer.io Campaign, add a Segment Filter on the deal_value attribute - it is updated on every stage change. The Campaign only targets people with deal_value >= 5000. Attributes are passed via the Track API using PUT /customers/{email}, which is exactly what we do in on_lead_status_changed.

Do I need a specific Customer.io plan to use Reporting Webhooks?

Reporting Webhooks are available on all paid Customer.io plans: Essentials and above. The Free plan does not support webhooks. Verify under Customer.io -> Settings -> Billing.

How does an unsubscribe in Customer.io show up in Kommo?

In our example, email_unsubscribed creates a note in Kommo and can update a custom subscription-status field. The logic is easy to extend: if a lead unsubscribes, automatically create a task for the manager to find out why, or move the deal to a “Declined” stage.

Can I sync Kommo custom fields with Customer.io attributes?

Yes. In get_kommo_lead_full, parse the custom_fields_values from Kommo and map the relevant fields to Customer.io attributes. For example, the “Industry” field from Kommo becomes the industry attribute in Customer.io for segmenting campaigns by vertical.

Summary

The Kommo + Customer.io bidirectional integration provides a complete picture: the CRM pipeline drives email automation, and engagement data flows back into the CRM. The scheme:

  • Kommo webhook (status change, new lead) -> Customer.io Track API: attribute update and trigger event
  • Customer.io Reporting Webhook -> Kommo: open note, hot-lead call task
  • Customer.io Campaigns segmented by deal attributes (deal_value, kommo_stage, industry)

If your team works with Kommo and Customer.io - describe your setup to the Exceltic.dev team. We will configure bidirectional sync tailored to your pipeline structure and campaigns.

More articles

All →