Discuss your task

Search

Start typing to search articles, cases, and services.

navigate Esc close

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
Get in touch your way
WhatsApp Telegram

Before you go: bonus months of Kommo

The implementation estimate is free: describe your sales process in a few words and we propose a solution, a plan and a quote with timelines.

We are an official Kommo partner in Europe: pay for the subscription through us and get bonus months.