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/reloadsafter 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.