Mixpanel is a product analytics platform with a server-side Python SDK: events from any source are sent via track(), user profiles via people_set(). Without a Kommo integration, Mixpanel only sees in-product behavior but does not know when a lead became a customer or how many days were spent in the pipeline. With the integration, stage change events, Won, and Lost from the CRM flow into Mixpanel, giving analysts an end-to-end picture from first touch to payment.
Why connecting Kommo and Mixpanel matters
Without integration:
— Mixpanel shows retention, activation, feature usage — but does not know which users became paying customers
— Kommo holds deal data, amounts, cycle length — but has no connection to product behavior
— The product manager cannot answer: “Clients who reached Won — at what onboarding stage were they?”
— Revenue attribution to marketing channels in Mixpanel does not close on actual deals
With integration:
— New contact in Kommo -> Mixpanel profile with CRM properties (source, manager, type)
— Stage change -> event Lead Stage Changed with stage name and deal ID
— Won -> event Deal Won with amount, cycle length, source
— Lost -> event Deal Lost with the reason from Kommo
— Funnels, cohort analysis, and retention on real deals are built in Mixpanel
What is synchronized
Kommo -> Mixpanel:
— New contact -> people_set with email, company, lead source, responsible manager
— Stage change -> track('Lead Stage Changed', {stage, deal_id, deal_value})
— Won -> track('Deal Won', {revenue, cycle_days, source, manager})
— Lost -> track('Deal Lost', {reason, stage_at_loss, cycle_days})
— Task created on contact -> optional track('CRM Task Created')
Mixpanel -> Kommo (optional):
— “Active users” cohort from Mixpanel -> tag on Kommo contact for manager prioritization
— Product activity drop -> Note in Kommo for a proactive CSM call
Architecture
Kommo Webhook: contact created / updated
↓ Backend
1. GET /api/v4/contacts/{id}
-> email (as distinct_id), name, company, source from custom fields
2. Mixpanel: people_set(distinct_id=email, properties)
-> $email, $name, crm_source, kommo_contact_id
Kommo Webhook: deal changed (stage / status)
↓ Backend
1. GET /api/v4/leads/{id} + contacts
-> stage, amount, contact, creation date
2. distinct_id = contact email
3. Mixpanel: track(distinct_id, event_name, properties)
-> For Won: 'Deal Won' + {revenue, cycle_days, source, manager}
-> For Lost: 'Deal Lost' + {reason, stage_at_loss}
-> For stage change: 'Lead Stage Changed' + {from_stage, to_stage, deal_id}
Mixpanel Python SDK: key requests
pip install mixpanel
Initialization (with EU support):
from mixpanel import Mixpanel, Consumer
MP_TOKEN = 'your_project_token'
MP_SECRET = 'your_api_secret' # for import_data
# EU data residency - required for EU markets (GDPR)
mp = Mixpanel(MP_TOKEN, consumer=Consumer(api_host='api-eu.mixpanel.com'))
# US (default)
# mp = Mixpanel(MP_TOKEN)
Create/update a contact profile:
def sync_contact_to_mixpanel(email: str, name: str, company: str,
source: str, kommo_id: int):
mp.people_set(email, {
'$email': email,
'$name': name,
'company': company,
'crm_source': source,
'kommo_contact_id': kommo_id
})
Send a stage change event:
from datetime import datetime, date
def track_stage_change(email: str, deal_id: int, deal_name: str,
from_stage: str, to_stage: str, deal_value: float):
mp.track(email, 'Lead Stage Changed', {
'deal_id': deal_id,
'deal_name': deal_name,
'from_stage': from_stage,
'to_stage': to_stage,
'deal_value': deal_value
})
def track_deal_won(email: str, deal_id: int, revenue: float,
source: str, manager: str, created_at: datetime):
cycle_days = (date.today() - created_at.date()).days
mp.track(email, 'Deal Won', {
'deal_id': deal_id,
'revenue': revenue,
'source': source,
'manager': manager,
'cycle_days': cycle_days
})
# Update profile - mark as paying customer
mp.people_set(email, {
'customer_status': 'paying',
'first_purchase_date': date.today().isoformat(),
'ltv': revenue
})
def track_deal_lost(email: str, deal_id: int, reason: str,
stage_at_loss: str, created_at: datetime):
cycle_days = (date.today() - created_at.date()).days
mp.track(email, 'Deal Lost', {
'deal_id': deal_id,
'loss_reason': reason,
'stage_at_loss': stage_at_loss,
'cycle_days': cycle_days
})
Import historical data (events older than 5 days):
def import_historical_event(email: str, event_name: str,
timestamp: int, properties: dict):
mp.import_data(
api_secret=MP_SECRET,
distinct_id=email,
event_name=event_name,
timestamp=timestamp, # Unix timestamp
properties=properties
)
import_data() is needed during the initial data population — when loading historical deals from Kommo over the past months. For real-time use track().
Kommo webhook handler:
from flask import Flask, request
app = Flask(__name__)
@app.route('/webhooks/kommo', methods=['POST'])
def kommo_webhook():
data = request.json
event_type = data.get('leads', {}).get('status')
if event_type:
for lead in event_type:
deal_id = lead.get('id')
# Get full deal and contact data
deal = get_kommo_lead(deal_id)
contact = get_lead_contact(deal)
if not contact or not contact.get('email'):
continue # cannot determine distinct_id without email
handle_deal_event(deal, contact)
return '', 200
def handle_deal_event(deal: dict, contact: dict):
email = contact['email']
status_id = deal.get('status_id')
if status_id == 142: # Won (standard ID in Kommo)
track_deal_won(email, deal['id'], deal.get('price', 0),
deal.get('source', ''), deal.get('manager', ''),
deal['created_at'])
elif status_id == 143: # Lost
loss_reason = deal.get('loss_reason', {}).get('name', '')
track_deal_lost(email, deal['id'], loss_reason,
deal.get('stage_name', ''), deal['created_at'])
else:
track_stage_change(email, deal['id'], deal.get('name', ''),
deal.get('prev_stage', ''), deal.get('stage_name', ''),
deal.get('price', 0))
Distinct ID: email as a universal identifier
Mixpanel uses distinct_id to link a profile to events. In a server-side integration with Kommo, using the contact’s email is the simplest approach — it is available in the CRM and already known to Mixpanel if the contact has previously interacted with the product.
Issue: if a contact registered in the product with one email but was recorded in Kommo with another — profiles will be split. Solution: $merge via the Mixpanel Identity API, or a strict policy of using a single email across all platforms.
If the product uses the Mixpanel JS SDK and distinct_id is generated automatically (anonymous ID) — store this ID in a Kommo custom field and pass it in events instead of the email.
Real-world case
B2B SaaS (EU, 150–200 trials per month, product and sales teams working independently):
- Before: Mixpanel showed the activation funnel, but the product team could not answer “how many activated users became paying customers”. Kommo stored Won deals, but without a connection to product behavior.
- After: every Won from Kommo -> event in Mixpanel with
cycle_daysandrevenue. In Mixpanel, a funnel was built: Trial Start -> Activation -> Deal Won. It turned out: users who completed onboarding in under 3 days convert at 2.3x the rate — this became the product team’s top priority. - Additionally: “no activity in 14 days” cohort from Mixpanel -> Note in Kommo -> CSM makes a proactive call before churn.
For deep SQL analytics on Kommo data without Mixpanel — see Redash. For ad channel attribution through to a closed deal — Prooflytics gives a more accurate picture than Mixpanel.
Who this is relevant for
- Company uses Mixpanel for product analytics and Kommo for sales
- Product team wants to see which in-product behavior patterns correlate with Won
- Sales team wants to receive signals from Mixpanel (activity drop, limit reached) in Kommo
- Cohort analytics on the deal cycle are needed: how many days from first touch to payment
Frequently asked questions
Mixpanel track() or import_data() — when to use which?
track() — for real-time events: accepts events no older than 5 days. import_data() — for historical data: any timestamp, requires api_secret (not the token), uses the /import endpoint. During initial population use import_data for historical records, then switch to track for ongoing events.
How do I configure EU data residency in Mixpanel?
When initializing the SDK, pass a custom Consumer: Mixpanel(token, consumer=Consumer(api_host='api-eu.mixpanel.com')). Without this, data goes to US servers — a GDPR violation for EU contacts. EU residency is configured at the project level in the Mixpanel UI and must match the SDK configuration.
What should be used as distinct_id?
For server-side CRM integration — the contact’s email: it is available in Kommo and allows merging the CRM profile with the product profile. If your product generates distinct_id on the client (anonymous UUID on first visit) — store it in a Kommo custom field and pass it in events. Email as distinct_id is simpler but requires a consistent email policy across all platforms.
Does Mixpanel accept events from the backend without the SDK?
Yes. Events can be sent directly via HTTP API: POST https://api.mixpanel.com/track (or api-eu.mixpanel.com/track) with a base64-encoded JSON. The Python SDK does this automatically — using the SDK rather than raw HTTP is recommended for correct error handling and retry logic.
Is a token or secret required for track()?
track() and people_set() use the project token (public, safe to store in code). import_data() requires the API secret (private, store in environment variables). Both values are available in Mixpanel: Settings -> Project Settings.
Summary
- Mixpanel Python SDK:
pip install mixpanel, EU consumer forapi-eu.mixpanel.com people_set()— contact profile from Kommo;track()— stage events, Won, Lostimport_data()with api_secret — for initial historical deal loadingdistinct_id= contact email — the simplest option for CRM integration- Funnels by CRM events and cohort deal cycle analysis are built in Mixpanel
- Typical development timeline — 1–2 weeks
If you use Mixpanel and Kommo and want an end-to-end picture from first touch to closed deal — describe your setup: which events matter and which deal properties are needed in analytics. Exceltic.dev will configure the mapping and initial history import.