Kommo + Mixpanel: sales pipeline events in product analytics

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_days and revenue. 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 for api-eu.mixpanel.com
  • people_set() — contact profile from Kommo; track() — stage events, Won, Lost
  • import_data() with api_secret — for initial historical deal loading
  • distinct_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.

More articles

All →