Kommo + Zoho Analytics: Sales BI Dashboard for Companies in the Zoho Ecosystem

Kommo + Zoho Analytics: Sales BI Dashboard for Companies in the Zoho Ecosystem

Zoho Analytics is a cloud-based BI tool within the Zoho ecosystem - it creates interactive reports and dashboards, supports SQL queries, and automatically refreshes data. Kommo is a CRM that holds sales pipeline data. Companies already using Zoho Books, Zoho Projects, or other Zoho tools often choose Zoho Analytics as their centralized BI layer - you can combine data from your CRM, accounting, and project management into a single dashboard.

The core challenge: Zoho Analytics has no native connector for Kommo (which is not Zoho CRM). Data from Kommo must be loaded via the Import API - similar to the PostgreSQL approach used for Tableau, but over HTTP instead of JDBC.

Zoho Analytics Import API lets you push data via POST /api/{email}/{dbName}/{tableName}, automatically creating tables on the first load. Authentication uses OAuth 2.0 (Client Credentials for server-to-server flows).

Architecture

Kommo REST API (incremental fetch by updated_at)
  -> Python ETL (cron every 2 hours)
  -> Zoho Analytics Import API (Bulk Import or Row API)
     Table: kommo_leads

Zoho Analytics
  -> Reports based on kommo_leads
  -> Dashboards: funnel, KPIs, revenue forecast

OAuth 2.0 Implementation for Zoho Analytics

import requests, os, json, time
from pathlib import Path

ZA_CLIENT_ID     = os.environ["ZOHO_CLIENT_ID"]
ZA_CLIENT_SECRET = os.environ["ZOHO_CLIENT_SECRET"]
ZA_REFRESH_TOKEN = os.environ["ZOHO_REFRESH_TOKEN"]
ZA_ORG_EMAIL     = os.environ["ZOHO_ORG_EMAIL"]
ZA_DB_NAME       = os.environ["ZOHO_DB_NAME"]     # database name in Zoho Analytics
ZA_TABLE_NAME    = "kommo_leads"

TOKEN_CACHE = Path("/tmp/zoho_token.json")

def get_zoho_token() -> str:
    # Check cached token
    if TOKEN_CACHE.exists():
        cached = json.loads(TOKEN_CACHE.read_text())
        if cached.get("expires_at", 0) > time.time() + 60:
            return cached["access_token"]

    # Refresh
    r = requests.post(
        "https://accounts.zoho.com/oauth/v2/token",
        data={
            "client_id":     ZA_CLIENT_ID,
            "client_secret": ZA_CLIENT_SECRET,
            "refresh_token": ZA_REFRESH_TOKEN,
            "grant_type":    "refresh_token",
        },
    )
    r.raise_for_status()
    data = r.json()

    token_data = {
        "access_token": data["access_token"],
        "expires_at":   time.time() + data.get("expires_in", 3600) - 30,
    }
    TOKEN_CACHE.write_text(json.dumps(token_data))
    return token_data["access_token"]

ETL: Kommo -> Zoho Analytics

KOMMO_DOMAIN = os.environ["KOMMO_DOMAIN"]
KOMMO_TOKEN  = os.environ["KOMMO_TOKEN"]
KOMMO_BASE   = f"https://{KOMMO_DOMAIN}/api/v4"
KOMMO_HDR    = {"Authorization": f"Bearer {KOMMO_TOKEN}"}

def fetch_leads_since(since_ts: int) -> list:
    leads, page = [], 1
    session = requests.Session()
    session.headers.update(KOMMO_HDR)

    while True:
        r = session.get(f"{KOMMO_BASE}/leads", params={
            "updated_at[from]": since_ts,
            "limit": 250,
            "page":  page,
        })
        if r.status_code == 204:
            break
        batch = r.json().get("_embedded", {}).get("leads", [])
        if not batch:
            break
        leads.extend(batch)
        if len(batch) < 250:
            break
        page += 1

    return leads

def leads_to_rows(leads: list) -> list[dict]:
    rows = []
    for lead in leads:
        closed = lead.get("closed_at")
        rows.append({
            "lead_id":           lead["id"],
            "name":              lead.get("name", ""),
            "price":             lead.get("price", 0),
            "status_id":         lead.get("status_id"),
            "pipeline_id":       lead.get("pipeline_id"),
            "responsible_id":    lead.get("responsible_user_id"),
            "created_at":        _ts(lead.get("created_at")),
            "updated_at":        _ts(lead.get("updated_at")),
            "closed_at":         _ts(closed) if closed else "",
        })
    return rows

def _ts(epoch: int | None) -> str:
    if not epoch:
        return ""
    from datetime import datetime, timezone
    return datetime.fromtimestamp(epoch, tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S")

def import_to_zoho(rows: list[dict]):
    if not rows:
        return

    token = get_zoho_token()

    # Format: JSON array
    import_data = json.dumps(rows)

    url = f"https://analyticsapi.zoho.com/api/{ZA_ORG_EMAIL}/{ZA_DB_NAME}/{ZA_TABLE_NAME}"
    r = requests.post(
        url,
        headers={"Authorization": f"Zoho-oauthtoken {token}"},
        params={
            "ZOHO_ACTION":      "IMPORT",
            "ZOHO_OUTPUT_FORMAT": "JSON",
            "ZOHO_ERROR_FORMAT": "JSON",
            "ZOHO_API_VERSION": "1.0",
            "ZOHO_IMPORT_TYPE": "UPDATEADD",   # Upsert: update or insert
            "ZOHO_AUTO_IDENTIFY": "TRUE",      # Auto-detect column types
        },
        data={"ZOHO_DO_IN_BACKGROUND": "false", "ZOHO_IMPORT_DATA": import_data},
    )
    result = r.json()
    summary = result.get("response", {}).get("result", {}).get("importSummary", {})
    print(f"Imported: {summary.get('totalRecordCount', 0)} records")

def run_etl():
    # Read last sync timestamp from file
    ts_file = Path("/tmp/kommo_last_sync.txt")
    since = int(ts_file.read_text()) if ts_file.exists() else 0

    leads = fetch_leads_since(since)
    if leads:
        rows = leads_to_rows(leads)
        import_to_zoho(rows)

        # Update timestamp
        max_ts = max(l.get("updated_at", 0) for l in leads)
        ts_file.write_text(str(max_ts))
        print(f"Synced {len(leads)} leads")

if __name__ == "__main__":
    run_etl()

Dashboards in Zoho Analytics

After the initial data load, create reports in Zoho Analytics:

  • Sales funnel: Count leads by status_id -> Bar chart
  • Revenue by manager: SUM(price) GROUP BY responsible_id -> Pie chart
  • Closing dynamics: COUNT(*) WHERE status_id=142 GROUP BY date(closed_at) -> Line chart
  • Average deal cycle: AVG(closed_at - created_at) WHERE status_id=142 -> KPI widget

Zoho Analytics supports SQL queries natively - all standard aggregations and JOINs with other tables (for example, data from Zoho Books) are available out of the box.

Real-World Use Case

A company using Zoho Books for accounting and Kommo for sales wanted to see revenue from Zoho Books and deal statuses from Kommo in a single dashboard. The solution: an ETL pipeline from Kommo to Zoho Analytics, combined with the native Zoho Books -> Zoho Analytics connector. The dashboard refreshes every two hours. The director can see funnel conversion rates and issued invoices in one place, without switching between systems.

Who This Is For

Companies already using other Zoho products - Books, Projects, Desk, Campaigns. If Zoho Analytics is already licensed for other data sources, adding Kommo as an additional source requires no extra licensing costs - only ETL development.

A similar approach for Tableau is covered in Kommo + Tableau: Sales BI Dashboard.

Frequently Asked Questions

What is the difference between UPDATEADD and other import modes?

Zoho Analytics supports several import modes: APPEND (add only), TRUNCATEADD (clear then add), and UPDATEADD (update existing records by key, or insert new ones). For incremental sync you need UPDATEADD with lead_id as the key column.

How do I set up automatic refresh without cron?

Zoho Analytics supports Data Sync - you can configure a schedule directly in the interface for SQL-based sources. For Kommo, you need either a Data Bridge (an on-premise component) or an external ETL. The simplest option is GitHub Actions or any other scheduler.

Are there rate limits on the Zoho Analytics Import API?

Yes: 10 requests per minute for the Import API, with a maximum of 25,000 rows per import call. For large data volumes, split imports into batches of 5,000 - 10,000 rows.

Summary

Kommo + Zoho Analytics:

  • OAuth 2.0 Client Credentials + Refresh Token for authentication
  • Kommo paginated API (updated_at[from]) for incremental data fetching
  • Import API with ZOHO_IMPORT_TYPE=UPDATEADD for upsert by lead_id
  • Zoho Analytics dashboards with SQL queries on top of Kommo data

If you use the Zoho ecosystem and want to combine Kommo CRM data with other Zoho services, reach out to Exceltic.dev.

More articles

All →