Kommo + Customer.io: triggered email sequences from the sales pipeline

Kommo + Customer.io: triggered email sequences from the sales pipeline

Customer.io is a behavioral email marketing platform with a Track API on Basic Auth. Without the Kommo integration, a marketer manually adds contacts to Customer.io, the manager does not know whether the client opened an email before a call, and a client unsubscribe does not update the CRM fields. With the integration, a pipeline stage change in Kommo automatically launches a Customer.io campaign — and every email interaction is reflected in the deal timeline.

What the Kommo + Customer.io combination delivers

Without integration: — Marketer manually maintains the Customer.io list — with a 1–3 day lag from the CRM status change — Manager does not know whether the client received the welcome email before the first call — Client unsubscribed in Customer.io -> Kommo does not know, manager sends again — Email sequences and CRM pipeline live in separate systems without connection

With integration: — Won in Kommo -> Customer.io: identify + event deal_won -> onboarding campaign launched — email.opened -> Note in Kommo: “Client opened email (subject)” — email.clicked -> Note: “Clicked link in email” — email.unsubscribed -> custom field email_status = unsubscribed + task for manager — email.bounced -> task to check contact email

What gets synchronized

Kommo -> Customer.io: — Contact email, name, phone -> Customer profile in Customer.io — Plan, segment from custom fields -> Customer attributes for email personalization — Pipeline stage change -> event -> campaign trigger — Deal ID -> Customer attribute kommo_deal_id for reverse tracing

Customer.io -> Kommo:email.opened -> Note in deal — email.clicked -> Note with the clicked link — email.unsubscribed -> field + task — email.bounced -> field + task to check email — email.converted (if goal is set) -> Note “Conversion from email”

Architecture

Kommo Webhook: deal moved to "Onboarding" stage
  ↓ Backend
  1. GET /api/v4/leads/{id} + contacts
     -> email, name, plan from custom fields
  2. Customer.io Track API: PUT /api/v1/customers/{email}
     -> initial identification with attributes
  3. Customer.io Track API: POST /api/v1/customers/{email}/events
     -> event: 'onboarding_started', properties: {plan, deal_id}
     -> triggers campaign with: event = 'onboarding_started'
  4. Kommo: POST /leads/{deal_id}/notes
     -> "Onboarding email campaign launched in Customer.io"

Customer.io Reporting Webhook: email.clicked
  ↓ Backend
  1. From payload: customer_id (email), subject, link_url, timestamp
  2. Find deal_id by email -> kommo_deal_id in Customer.io or via Kommo contact
  3. Kommo: POST /leads/{deal_id}/notes

Customer.io Reporting Webhook: email.unsubscribed
  ↓ Backend
  1. customer_id, timestamp
  2. Find deal_id
  3. Kommo: PATCH /leads/{deal_id} -> email_status = unsubscribed
  4. Kommo: POST /tasks -> "Client unsubscribed - clarify preferences"

Customer.io Track API: key requests

Track API Base URL: https://track.customer.io/api/v1/. Authentication: Basic Auth, username = site_id, password = api_key. Both values come from Customer.io: Settings -> API Credentials.

Identifying a user:

import requests
from requests.auth import HTTPBasicAuth

CIO_SITE_ID = 'your_site_id'
CIO_API_KEY = 'your_api_key'
CIO_AUTH = HTTPBasicAuth(CIO_SITE_ID, CIO_API_KEY)
CIO_TRACK_URL = 'https://track.customer.io/api/v1'

def identify_customer(email: str, attributes: dict) -> None:
    # Create or update profile in Customer.io
    resp = requests.put(
        f'{CIO_TRACK_URL}/customers/{email}',
        auth=CIO_AUTH,
        json=attributes
    )
    resp.raise_for_status()

Sending an event:

def track_event(email: str, event_name: str, data: dict = None) -> None:
    # Send event to trigger a campaign
    resp = requests.post(
        f'{CIO_TRACK_URL}/customers/{email}/events',
        auth=CIO_AUTH,
        json={'name': event_name, 'data': data or {}}
    )
    resp.raise_for_status()

def on_deal_won(lead: dict, contact: dict):
    email = get_contact_email(contact)
    plan = get_custom_field(lead, field_id=PLAN_FIELD_ID)

    identify_customer(email, {
        'first_name': contact['name'].split()[0],
        'plan': plan,
        'kommo_deal_id': lead['id'],
        'deal_amount': lead.get('price', 0)
    })

    track_event(email, 'deal_won', {'plan': plan, 'deal_id': lead['id']})

Kommo -> Customer.io event mapping:

STAGE_EVENT_MAP = {
    '12345678': 'demo_scheduled',
    '12345679': 'demo_completed',
    '12345680': 'proposal_sent',
    '12345681': 'deal_won',
}

def on_stage_change(lead_id: int, new_status_id: int):
    event_name = STAGE_EVENT_MAP.get(str(new_status_id))
    if not event_name:
        return
    lead = get_kommo_lead(lead_id)
    contact = get_kommo_contact(lead_id)
    email = get_contact_email(contact)
    track_event(email, event_name, {'deal_id': lead_id})

Handling Customer.io Reporting Webhooks:

from flask import Flask, request

app = Flask(__name__)

@app.route('/webhooks/customerio', methods=['POST'])
def customerio_webhook():
    payload = request.json
    event_type = payload.get('event_type')
    customer_id = payload.get('data', {}).get('customer_id')

    deal_id = find_kommo_deal_by_email(customer_id)
    if not deal_id:
        return '', 200

    if event_type == 'email_opened':
        subject = payload.get('data', {}).get('subject', '')
        create_kommo_note(deal_id, f'Customer.io: opened email - {subject}')

    elif event_type == 'email_clicked':
        link = payload.get('data', {}).get('href', '')
        subject = payload.get('data', {}).get('subject', '')
        create_kommo_note(deal_id, f'Customer.io: clicked in email {subject} -> {link}')

    elif event_type == 'email_unsubscribed':
        update_kommo_deal(deal_id, {'email_status': 'unsubscribed'})
        create_kommo_task(deal_id,
            'Client unsubscribed from Customer.io emails - clarify preferences')

    elif event_type == 'email_bounced':
        bounce_type = payload.get('data', {}).get('type', '')
        update_kommo_deal(deal_id, {'email_status': f'bounced_{bounce_type}'})
        create_kommo_task(deal_id,
            f'Email not delivered (bounce: {bounce_type}) - check address')

    return '', 200

Reporting Webhook setup in Customer.io: Settings -> Integrations -> Reporting Webhooks -> Add Endpoint. Select events: email_opened, email_clicked, email_unsubscribed, email_bounced, email_converted. Customer.io signs the payload via HMAC-SHA256, with the X-CIO-Signature header for verification.

EU instance: if the account is on EU servers — Track API URL: https://track-eu.customer.io/api/v1. Check in settings: Settings -> Account -> Data Region.

Personalization through Kommo attributes

Customer.io allows using customer attributes in email templates via Liquid: {{ customer.plan }}, {{ customer.first_name }}. Data from Kommo is passed via identify_customer:

identify_customer(email, {
    'first_name': 'John',
    'plan': 'Pro',
    'company': 'Acme Inc',
    'deal_amount': 1500,
    'account_manager': 'Sarah'
})

This gives email personalization without extra effort — all CRM variables are already in the profile.

Real case

B2B SaaS (US, 50–70 new clients per month, Kommo + Customer.io + Stripe, 7-email onboarding campaign):

  • Before: the marketer added new clients to Customer.io manually once a day from a Kommo export. The first email was sent with a 24–48 hour delay from the Won moment. Unsubscribes did not reach the CRM.
  • After: Won -> Customer.io -> first email within 3 minutes. Personalized by plan and name. When a client clicks on a documentation link — Note in Kommo, manager sees client activity.
  • Additionally: if the client did not open any email within 5 days — trigger onboarding_inactive -> task for CSM for manual outreach.

Who should use this

  • SaaS companies with active email onboarding (welcome series, educational campaigns)
  • Sales via Kommo pipeline, marketing via Customer.io
  • Manager needs to see client email activity before a call
  • 30+ new clients per month — manual synchronization becomes unprofitable

Frequently asked questions

Customer.io API — Basic Auth or Bearer token?

Track API (for identify and events) uses Basic Auth: site_id as username, api_key as password. App API (for managing campaigns and segments) uses Bearer token. For server-side integration with Kommo you need the Track API — Basic Auth.

How do you configure Reporting Webhooks in Customer.io?

Settings -> Integrations -> Reporting Webhooks -> Add Endpoint. Specify your server URL, select events. Customer.io signs each request via HMAC-SHA256 (X-CIO-Signature). Verify the signature on the server before processing.

Customer.io vs Mailchimp for Kommo integration?

Mailchimp is focused on list campaigns. Customer.io is focused on behavioral triggers: CRM event -> campaign. If you need onboarding based on pipeline stages, sequences based on product actions — Customer.io. If you need mass campaigns by segment — Mailchimp.

What if a contact already exists in Customer.io before the integration?

PUT /customers/{email} is idempotent: if the profile exists — it updates attributes; if not — it creates it. Attributes are merged, not fully overwritten.

How do you pass data from Kommo custom fields to Customer.io?

Via identify_customer, you pass an arbitrary dictionary of attributes. For each required Kommo custom field, create a mapping from field_id to Customer.io attribute name. Attributes are immediately available in Liquid email templates.

Summary

  • Customer.io Track API: Basic Auth (site_id:api_key), Base URL https://track.customer.io/api/v1/
  • Identify: PUT /customers/{email} with attributes from Kommo
  • Track event: POST /customers/{email}/events on stage change
  • Reporting Webhook: email_opened/clicked/unsubscribed/bounced -> Notes and tasks in Kommo
  • EU instance: https://track-eu.customer.io/api/v1
  • Typical development timeline — 1–2 weeks

If you use Customer.io and Kommo and want to connect the sales pipeline with email marketing — describe your stages and campaigns. Exceltic.dev will configure the event mapping and unsubscribe handling in the CRM.

More articles

All →