Kommo + Geckoboard: Real-Time Sales KPI Dashboard

Kommo + Geckoboard: Real-Time Sales KPI Dashboard

Integrating Kommo and Geckoboard via the Datasets API lets you display CRM data on an office TV dashboard or a shared link for a remote team: pipeline by stage, conversion rate, month-to-date revenue, and a manager leaderboard. Data is updated on a schedule every 15 minutes via a Python ETL script.

Sales managers want to see the pipeline on a shared screen without opening the CRM every day. Kommo has no native export to Geckoboard. Teams work around this in various ways: homemade Google Sheets with manual updates, daily CSV exports, or sometimes Databox with a limited set of metrics. Geckoboard via the Datasets API is a more flexible approach: you define the data schema, metrics, and update frequency yourself. Below is the architecture, code, and concrete widgets for a sales team.

Why There Is No Native Kommo and Geckoboard Integration

Geckoboard supports 80+ native connectors to popular SaaS tools: Salesforce, HubSpot, Zendesk, Google Analytics. Kommo is not on the list. The only path is the Geckoboard Datasets API: you push data to a named dataset via REST yourself, and Geckoboard builds widgets on top of it.

This is actually the better solution. A native Kommo connector, if it existed, would offer a fixed set of metrics. The Datasets API gives full freedom: you choose what to pull from the Kommo API, how to aggregate it, and at what granularity to display it. Pipeline velocity, average deal age by stage, win rate by lead source - these are not standard Kommo metrics, but through an ETL they become available.

Integration Architecture

Stack: Python 3.11, Kommo API v4, Geckoboard Datasets API, APScheduler for scheduling. Data is not stored in a database - every 15 minutes the ETL script takes a full snapshot of the current pipeline state and pushes it to Geckoboard.

Geckoboard Auth: API Key in Basic Auth. In HTTP requests: username = API Key, password = empty string. The key is found in your Geckoboard account: profile icon -> Account -> API Key.

Geckoboard Datasets API: endpoint PUT /datasets/{dataset-id} - creates or updates a dataset. POST /datasets/{dataset-id}/data - appends records. For a real-time dashboard use PUT /datasets/{dataset-id}/data with the delete_by parameter to replace previous data.

Metrics for the sales dashboard:

  • deals_by_stage - number and value of deals by funnel stage
  • revenue_mtd - current month revenue (Won deals)
  • win_rate_by_source - conversion rate by lead source
  • pipeline_velocity - average deal age by stage (days)
  • leaderboard - deals won and revenue by manager for the current period
import logging
import requests
from datetime import datetime, timezone, date
from apscheduler.schedulers.blocking import BlockingScheduler
from collections import defaultdict

logging.basicConfig(level=logging.INFO)

KOMMO_BASE = "https://your-domain.kommo.com"
KOMMO_TOKEN = "your-kommo-token"
GECKOBOARD_API_KEY = "your-geckoboard-api-key"
GECKOBOARD_BASE = "https://api.geckoboard.com"

# Dataset names in Geckoboard (created automatically on first push)
DS_PIPELINE = "kommo.pipeline_by_stage"
DS_REVENUE = "kommo.revenue_mtd"
DS_LEADERBOARD = "kommo.leaderboard"

def geckoboard_session() -> requests.Session:
    """HTTP session with Basic Auth for Geckoboard."""
    s = requests.Session()
    s.auth = (GECKOBOARD_API_KEY, "")  # password is empty string
    s.headers.update({"Content-Type": "application/json"})
    return s

def kommo_get(path: str, params: dict = None) -> dict:
    """GET request to the Kommo API with authorization."""
    url = f"{KOMMO_BASE}/api/v4/{path}"
    headers = {"Authorization": f"Bearer {KOMMO_TOKEN}"}
    resp = requests.get(url, headers=headers, params=params or {}, timeout=15)
    resp.raise_for_status()
    return resp.json()

def get_all_leads(params: dict = None) -> list[dict]:
    """
    Fetch all deals with pagination.
    Kommo API returns a maximum of 250 records per request.
    """
    leads = []
    page = 1
    base_params = params or {}

    while True:
        p = {**base_params, "limit": 250, "page": page}
        data = kommo_get("leads", params=p)
        items = data.get("_embedded", {}).get("leads", [])
        if not items:
            break
        leads.extend(items)
        page += 1
        if len(items) < 250:  # Last page
            break

    return leads

def get_pipeline_stages() -> dict[int, str]:
    """Get mapping of status_id -> stage name."""
    data = kommo_get("leads/pipelines")
    stages = {}
    for pipeline in data.get("_embedded", {}).get("pipelines", []):
        for status in pipeline.get("_embedded", {}).get("statuses", []):
            stages[status["id"]] = status["name"]
    return stages

def compute_pipeline_by_stage(leads: list[dict], stages: dict) -> list[dict]:
    """Aggregate deals by stage: count and total value."""
    stage_data = defaultdict(lambda: {"count": 0, "total": 0.0})

    for lead in leads:
        status_id = lead.get("status_id")
        stage_name = stages.get(status_id, f"Stage {status_id}")
        stage_data[stage_name]["count"] += 1
        stage_data[stage_name]["total"] += float(lead.get("price", 0))

    return [
        {
            "stage": stage,
            "deals_count": data["count"],
            "pipeline_value": round(data["total"], 2)
        }
        for stage, data in stage_data.items()
    ]

def compute_revenue_mtd(leads: list[dict]) -> list[dict]:
    """Month-to-date revenue: Won deals only."""
    now = datetime.now(timezone.utc)
    month_start_ts = int(datetime(now.year, now.month, 1, tzinfo=timezone.utc).timestamp())

    revenue_by_day = defaultdict(float)

    for lead in leads:
        if lead.get("status_id") != 142:  # Your Won status_id
            continue
        closed_at = lead.get("closed_at", 0)
        if closed_at < month_start_ts:
            continue
        day = datetime.fromtimestamp(closed_at, tz=timezone.utc).strftime("%Y-%m-%d")
        revenue_by_day[day] += float(lead.get("price", 0))

    return [
        {"date": day, "revenue": round(val, 2)}
        for day, val in sorted(revenue_by_day.items())
    ]

def compute_leaderboard(leads: list[dict]) -> list[dict]:
    """
    Manager leaderboard: Won deals for the current month.
    Uses responsible_user_id from the deal.
    """
    now = datetime.now(timezone.utc)
    month_start_ts = int(datetime(now.year, now.month, 1, tzinfo=timezone.utc).timestamp())

    user_data = defaultdict(lambda: {"deals_won": 0, "revenue": 0.0})

    for lead in leads:
        if lead.get("status_id") != 142:
            continue
        closed_at = lead.get("closed_at", 0)
        if closed_at < month_start_ts:
            continue
        user_id = lead.get("responsible_user_id", "unknown")
        user_data[user_id]["deals_won"] += 1
        user_data[user_id]["revenue"] += float(lead.get("price", 0))

    # Get manager names
    users = kommo_get("users")
    user_names = {
        u["id"]: u["name"]
        for u in users.get("_embedded", {}).get("users", [])
    }

    return sorted(
        [
            {
                "manager": user_names.get(int(uid), f"User {uid}"),
                "deals_won": data["deals_won"],
                "revenue": round(data["revenue"], 2)
            }
            for uid, data in user_data.items()
        ],
        key=lambda x: x["revenue"],
        reverse=True
    )

def push_to_geckoboard(session: requests.Session, dataset_id: str, schema: dict, data: list[dict]):
    """
    Create or update a dataset in Geckoboard.
    PUT /datasets/{id} - creates the schema.
    PUT /datasets/{id}/data - replaces all data.
    """
    # 1. Ensure the dataset exists (create if not)
    resp = session.put(
        f"{GECKOBOARD_BASE}/datasets/{dataset_id}",
        json={"fields": schema}
    )
    resp.raise_for_status()

    # 2. Replace data
    resp = session.put(
        f"{GECKOBOARD_BASE}/datasets/{dataset_id}/data",
        json={"data": data}
    )
    resp.raise_for_status()
    logging.info(f"Pushed {len(data)} rows to Geckoboard dataset '{dataset_id}'")

def run_etl():
    """Main ETL function: Kommo -> Geckoboard."""
    logging.info("ETL run started")
    gb_session = geckoboard_session()

    try:
        stages = get_pipeline_stages()
        # Fetch active deals (not Won/Lost)
        active_leads = get_all_leads(params={"filter[statuses][0][pipeline_id]": "your-pipeline-id"})
        # Fetch Won deals for the current month for revenue and leaderboard
        all_leads = get_all_leads()  # In production, add a date filter

        # Pipeline by stage
        pipeline_data = compute_pipeline_by_stage(active_leads, stages)
        push_to_geckoboard(
            gb_session,
            DS_PIPELINE,
            schema={
                "stage": {"type": "string", "name": "Stage"},
                "deals_count": {"type": "number", "name": "Deals"},
                "pipeline_value": {"type": "money", "name": "Pipeline Value", "currency_code": "EUR"}
            },
            data=pipeline_data
        )

        # Revenue MTD
        revenue_data = compute_revenue_mtd(all_leads)
        push_to_geckoboard(
            gb_session,
            DS_REVENUE,
            schema={
                "date": {"type": "date", "name": "Date"},
                "revenue": {"type": "money", "name": "Revenue", "currency_code": "EUR"}
            },
            data=revenue_data
        )

        # Leaderboard
        leaderboard_data = compute_leaderboard(all_leads)
        push_to_geckoboard(
            gb_session,
            DS_LEADERBOARD,
            schema={
                "manager": {"type": "string", "name": "Manager"},
                "deals_won": {"type": "number", "name": "Deals Won"},
                "revenue": {"type": "money", "name": "Revenue", "currency_code": "EUR"}
            },
            data=leaderboard_data
        )

        logging.info("ETL run completed successfully")

    except Exception as e:
        logging.exception(f"ETL run failed: {e}")

if __name__ == "__main__":
    # Run on schedule: every 15 minutes
    scheduler = BlockingScheduler()
    scheduler.add_job(run_etl, "interval", minutes=15)
    run_etl()  # First run immediately
    scheduler.start()

Step-by-Step Implementation

Step 1. Get the Geckoboard API Key

In Geckoboard: profile icon in the top right -> Account -> scroll down to API Key. The key is used as the username in Basic Auth. Geckoboard Datasets API documentation: developer.geckoboard.com.

Step 2. Define the Dataset Schema

Geckoboard supports the following field types: string, number, money, percentage, date, datetime. The schema is fixed when the dataset is created via PUT /datasets/{id}. If you need to change the schema, delete the dataset via DELETE /datasets/{id} and recreate it.

Step 3. Find the Won status_id in Kommo

The status_id for Won is unique to each Kommo account. Retrieve it via GET /api/v4/leads/pipelines - the response contains a statuses array with a type: "won" field. Use this ID in your filter functions.

Step 4. Deploy the ETL as a Service

Run the Python script as a systemd service or in Docker. APScheduler triggers the ETL every 15 minutes. For production, add an alert on ETL failure (e.g., via Sentry or a Slack webhook) - if the script crashes, the dashboard will show stale data without any warning.

Step 5. Configure Widgets in Geckoboard

In Geckoboard: Add widget -> Datasets -> select your dataset -> choose a widget type. For pipeline_by_stage a Bar Chart or Column Chart works well. For leaderboard use the Leaderboard widget (supports ranking). For revenue_mtd use a Line Chart with accumulation.

Real-World Case with Numbers

B2B sales team in London, 12 people (4 SDR + 4 AE + Head of Sales + ops stack). Before integration: the Head of Sales manually compiled a daily report from Kommo into Google Sheets and sent it by email. Data was updated once a day; the office TV screen showed a static slide from the last meeting.

After integration: the office TV screen shows a live pipeline - which deals are at which stage, week-to-date revenue, and an AE leaderboard. Updates every 15 minutes. The Head of Sales stopped spending 30-45 minutes every morning assembling the report. The remote team got a shared Geckoboard link - the same screen available from any device.

Behavioral effect: AEs started monitoring their own leaderboard position and keeping Kommo data current without reminders. CRM data quality improved as a side effect of transparency.

Who This Is For

The Kommo + Geckoboard integration suits B2B sales teams of 6-30 people for whom team pipeline visibility matters without daily reporting meetings. Geckoboard is especially convenient for offices with a TV screen and for remote teams with distributed visibility through shared dashboards.

If you already use other BI tools for sales analytics, compare with Kommo + Metabase (SQL queries against CRM data, historical trends) and Kommo + Grafana (time-series metrics, alerting). Geckoboard is simpler to set up widgets, but less flexible for analysis. Metabase and Grafana offer more analytical power but require an intermediate database.

For custom Kommo integrations this type of task is standard: ETL from CRM to a dashboard tool with a flexible metrics schema.

Frequently Asked Questions

How does Geckoboard authenticate requests to the Datasets API?

Geckoboard uses HTTP Basic Auth: API Key as username, empty string as password. In Python requests: session.auth = (GECKOBOARD_API_KEY, ""). The API Key is found in your account: profile icon -> Account -> API Key. Rate limit: 60 requests per minute per key; exceeding it returns 429. Documentation: developer.geckoboard.com.

How often should data be updated and how many API calls does it cost?

Geckoboard Datasets API does not charge per request - cost is fixed by account plan. Limit: 60 requests per minute. Updating 3-5 datasets every 15 minutes means roughly 10-15 requests per ETL cycle - well below the rate limit. Kommo API has its own rate limit: 7 requests per second per account; account for this when paginating through large deal volumes.

Can data from Kommo and other systems be combined on one dashboard?

Yes. Geckoboard allows you to create an unlimited number of datasets from different sources and place widgets on a single dashboard. For example: pipeline from Kommo + support tickets from Intercom + revenue from Stripe on one TV screen. Each dataset is updated independently by its own ETL script.

How do you set up a separate leaderboard for SDRs and AEs?

In Kommo you can segment managers by tags, teams, or pipeline. If you have multiple pipelines (one for SDR outbound, another for AE closing), filter deals by pipeline_id when querying the Kommo API. In Geckoboard create two separate datasets: kommo.leaderboard_sdr and kommo.leaderboard_ae, and place two Leaderboard widgets side by side.

What happens to the dashboard if the ETL script crashes?

Geckoboard continues to show the last loaded data. Widgets do not disappear or display an error automatically - this can mislead the team if data is several hours stale. Recommendation: add monitoring with an alert on failure (Sentry, Slack webhook), and use the “Last updated” timestamp on Geckoboard widgets via a datetime field.

What’s Next

If your sales team makes decisions based on data that updates once a day or less - a live dashboard changes the working culture faster than any process change.

Describe your task to the Exceltic.dev team: which metrics you need on the dashboard, how many managers are on the team, and whether you need a TV screen or a shared link. We will assess the scope and propose a concrete dataset schema.

More articles

All →