Kommo + Qlik Sense: Enterprise BI Sales Dashboard from CRM Data

Qlik Sense is an enterprise BI platform built on the Associative Engine. Unlike Tableau or Power BI, Qlik builds a relationship graph across all your data - analysts can navigate from any field to any other without writing JOIN queries manually. For companies already running Qlik in their corporate environment, Kommo data needs to live alongside other sources in a shared BI layer. A custom integration lets you sync deals, contacts, and activities from Kommo into Qlik via the Qlik Cloud Data Files API or directly through the Qlik Engine.

Qlik Cloud API uses a Bearer Token (API Key from Qlik Cloud). Data is loaded as CSV via the Data Files API or through the Qlik REST Connector (configured in Qlik Data Integration). You can automate updates using Qlik Application Automation (low-code) or a custom script with cron.

Qlik Associative Engine - Qlik’s data engine: selecting any value in any dashboard automatically filters all related data. This is the key differentiator from the SQL-based approach used by Tableau and Power BI.

Architecture for B2B Sales

Two approaches depending on your infrastructure:

Approach 1 (recommended): ETL -> Qlik Data File

Your ETL service (cron every 2 hours)
  -> Kommo API: export leads, contacts, activities
  -> Build CSV with required fields
  -> Qlik Cloud API: POST /api/v1/data-files (upload CSV)
  -> Qlik app uses this file as a data source

Approach 2: PostgreSQL as an intermediate layer

ETL service -> PostgreSQL (upsert deals)
-> Qlik REST Connector -> PostgreSQL
-> Qlik app built on top of PostgreSQL

Approach 1 is simpler and requires no PostgreSQL. Approach 2 is more reliable at volumes above 50,000 deals.

Implementation: ETL from Kommo + Upload to Qlik

import requests, os, io, csv, time
from datetime import datetime, timezone

KOMMO_SUBDOMAIN = os.environ["KOMMO_SUBDOMAIN"]
KOMMO_TOKEN     = os.environ["KOMMO_ACCESS_TOKEN"]
QLIK_TENANT     = os.environ["QLIK_TENANT"]      # mycompany.eu.qlikcloud.com
QLIK_API_KEY    = os.environ["QLIK_API_KEY"]
QLIK_FILE_NAME  = os.environ.get("QLIK_FILE_NAME", "kommo_deals.csv")
QLIK_SPACE_ID   = os.environ.get("QLIK_SPACE_ID", "")  # personal space or team space

KOMMO_BASE = f"https://{KOMMO_SUBDOMAIN}.kommo.com/api/v4"
KOMMO_HDR  = {"Authorization": f"Bearer {KOMMO_TOKEN}"}

QLIK_BASE  = f"https://{QLIK_TENANT}"
QLIK_HDR   = {"Authorization": f"Bearer {QLIK_API_KEY}"}

def fetch_kommo_leads(page: int = 1, limit: int = 250) -> list[dict]:
    all_leads = []
    while True:
        r = requests.get(
            f"{KOMMO_BASE}/leads",
            headers=KOMMO_HDR,
            params={
                "with":   "contacts,custom_fields_values",
                "limit":  limit,
                "page":   page,
                "filter[updated_at][from]": int(time.time()) - 7 * 86400,  # last 7 days
            },
        )
        if r.status_code == 204:
            break
        data  = r.json()
        leads = data.get("_embedded", {}).get("leads", [])
        all_leads.extend(leads)
        if len(leads) < limit:
            break
        page += 1
    return all_leads

def leads_to_csv(leads: list[dict]) -> bytes:
    output = io.StringIO()
    writer = csv.writer(output)
    writer.writerow([
        "lead_id", "lead_name", "status_id", "pipeline_id",
        "price", "responsible_user_id", "created_at", "updated_at",
        "contact_name", "contact_email",
    ])
    for lead in leads:
        contacts     = lead.get("_embedded", {}).get("contacts", [{}])
        contact_name = contacts[0].get("name", "") if contacts else ""

        email = ""
        for cf in lead.get("custom_fields_values", []) or []:
            if cf.get("field_code") == "EMAIL":
                vals = cf.get("values", [])
                if vals:
                    email = vals[0].get("value", "")
                    break

        writer.writerow([
            lead.get("id"),
            lead.get("name"),
            lead.get("status_id"),
            lead.get("pipeline_id"),
            lead.get("price", 0),
            lead.get("responsible_user_id"),
            datetime.fromtimestamp(lead.get("created_at", 0), tz=timezone.utc).isoformat(),
            datetime.fromtimestamp(lead.get("updated_at", 0), tz=timezone.utc).isoformat(),
            contact_name,
            email,
        ])
    return output.getvalue().encode("utf-8")

def upload_to_qlik(csv_bytes: bytes, file_name: str) -> str:
    # Check if the file already exists
    params = {"name": file_name}
    if QLIK_SPACE_ID:
        params["spaceId"] = QLIK_SPACE_ID

    r_list = requests.get(f"{QLIK_BASE}/api/v1/data-files", headers=QLIK_HDR, params=params)
    existing = r_list.json().get("data", [])

    if existing:
        # Update the existing file
        file_id = existing[0]["id"]
        r = requests.put(
            f"{QLIK_BASE}/api/v1/data-files/{file_id}",
            headers=QLIK_HDR,
            files={"File": (file_name, csv_bytes, "text/csv")},
        )
    else:
        # Create a new file
        data = {"name": file_name}
        if QLIK_SPACE_ID:
            data["spaceId"] = QLIK_SPACE_ID
        r = requests.post(
            f"{QLIK_BASE}/api/v1/data-files",
            headers=QLIK_HDR,
            files={"File": (file_name, csv_bytes, "text/csv")},
            data=data,
        )

    r.raise_for_status()
    return r.json().get("id", "")

def reload_qlik_app(app_id: str):
    r = requests.post(
        f"{QLIK_BASE}/api/v1/reloads",
        headers={**QLIK_HDR, "Content-Type": "application/json"},
        json={"appId": app_id, "partial": False},
    )
    return r.json().get("id", "")

def run_etl():
    leads    = fetch_kommo_leads()
    csv_data = leads_to_csv(leads)
    file_id  = upload_to_qlik(csv_data, QLIK_FILE_NAME)
    print(f"Uploaded {len(leads)} deals to Qlik. File ID: {file_id}")
    return file_id

if __name__ == "__main__":
    run_etl()

Schedule: Cron Every 2 Hours

0 */2 * * * cd /app && python etl_kommo_qlik.py >> /var/log/kommo_qlik.log 2>&1

Or use Qlik Application Automation: create an Automation with a scheduled trigger that calls your ETL endpoint via an HTTP action.

Key Metrics for Your Qlik Sales Dashboard

In the Qlik Load Script, connect the CSV and create the following Measures:

  • Win Rate: Count({<status_id={'won'}>} lead_id) / Count(lead_id) * 100
  • Average Deal Size: Avg({<status_id={'won'}>} price)
  • Pipeline Velocity: Sum(price) / Count(distinct responsible_user_id) / Days
  • Stage Conversion: Count by each status_id with period-over-period comparison
  • Revenue Forecast: Sum price by pipeline_id for open deals

Real-World Case

A company with 3 pipelines in Kommo processing 200+ deals per month. Qlik Sense was already in use for financial analytics. Kommo data needed to sit alongside financial data in a single dashboard. A custom ETL uploads data every 2 hours. The CFO now sees Revenue Forecast next to P&L in a unified Qlik application.

Who This Is For

Enterprise companies that already use Qlik Sense as their corporate BI standard. If Qlik is licensed at the company level, adding Kommo data requires minimal development effort. For companies without an existing BI platform, Tableau or Metabase are easier to get started with.

A similar approach is described for Kommo + Tableau and Kommo + Zoho Analytics.

Frequently Asked Questions

Where do I get a Qlik Cloud API Key?

Qlik Cloud -> Management Console -> API Keys -> Create New Key. The key is created with your user’s permissions. For production, use a service account with minimum required permissions: Data Files (read/write), Apps (read, reload).

Qlik On-Premise vs Qlik Cloud: are the APIs different?

Qlik Cloud uses a REST API (described above). Qlik Enterprise on-premise uses the Engine API (WebSocket-based, QRPC) - a fundamentally different approach. If you are running Qlik Sense on-premise, data interaction happens through QVD files or a REST Connector configured in the Data Load Editor.

How do I load only changed data (incremental load)?

Filter Kommo by updated_at: pass the timestamp of the last sync in filter[updated_at][from]. Store the timestamp in a file or Redis. In the Qlik Load Script: LOAD * FROM ... WHERE lead_id NOT EXISTS in old QVD + concatenate. A full reload is simpler; incremental load is only necessary above 100,000 deals.

Summary

Kommo + Qlik Sense - enterprise BI from CRM data:

  • ETL: Kommo API with=contacts,custom_fields_values -> CSV -> Qlik Data Files
  • POST /api/v1/data-files (create) / PUT /api/v1/data-files/{id} (update)
  • Bearer API Key, Space ID for team spaces
  • App reload: POST /api/v1/reloads after file upload
  • Cron every 2 hours to keep data current

If your company runs Qlik and you need a Kommo integration - describe your task to the Exceltic.dev team.

More articles

All →