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}/eventson 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.