Kommo + Geckoboard: KPI-дашборд продаж в реальном времени
Интеграция Kommo и Geckoboard через Datasets API позволяет отображать данные из CRM на TV-дашборде офиса или в shared link для remote-команды: pipeline по этапам, conversion rate, revenue месяца, leaderboard по менеджерам. Данные обновляются по расписанию каждые 15 минут через Python ETL-скрипт.
Руководители продаж хотят видеть pipeline на общем экране без ежедневного открытия CRM. Kommo не имеет нативного экспорта в Geckoboard. Команды решают это по-разному: самодельные Google Sheets с ручным обновлением, CSV-экспорт раз в день, иногда Databox с ограниченным набором метрик. Geckoboard через Datasets API - более гибкий подход: вы сами определяете схему данных, метрики и частоту обновления. Ниже - архитектура, код и конкретные виджеты для команды продаж.
Почему нативной интеграции Kommo и Geckoboard не существует
Geckoboard поддерживает 80+ нативных коннекторов к популярным SaaS: Salesforce, HubSpot, Zendesk, Google Analytics. Kommo в этом списке нет. Единственный путь - Geckoboard Datasets API: вы самостоятельно пушите данные в именованный dataset через REST, а Geckoboard строит виджеты на его основе.
Это на самом деле лучшее решение. Нативный коннектор к Kommo, если бы он существовал, предлагал бы фиксированный набор метрик. Datasets API дает полную свободу: вы сами выбираете какие данные тянуть из Kommo API, как агрегировать и с какой гранулярностью отображать. Pipeline velocity, average deal age by stage, win rate by lead source - это не стандартные метрики Kommo, но через ETL они становятся доступными.
Архитектура интеграции
Стек: Python 3.11, Kommo API v4, Geckoboard Datasets API, APScheduler для расписания. Данные не хранятся в БД - каждые 15 минут ETL-скрипт делает полный снапшот текущего состояния pipeline и пушит в Geckoboard.
Auth Geckoboard: API Key в Basic Auth. При HTTP-запросе: username = API Key, password = пустая строка. Ключ находится в аккаунте Geckoboard: иконка профиля -> Account -> API Key.
Geckoboard Datasets API: endpoint PUT /datasets/{dataset-id} - создаёт или обновляет dataset. POST /datasets/{dataset-id}/data - добавляет записи. Для real-time дашборда используйте PUT /datasets/{dataset-id}/data с параметром delete_by для замены предыдущих данных.
Метрики для дашборда продаж:
deals_by_stage- количество и сумма сделок по этапам воронкиrevenue_mtd- выручка текущего месяца (Won сделки)win_rate_by_source- конверсия по источникам лидовpipeline_velocity- средний возраст сделки по этапам (дни)leaderboard- deals won и revenue по менеджерам за текущий период
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"
# Имена datasets в Geckoboard (создаются автоматически при первом push)
DS_PIPELINE = "kommo.pipeline_by_stage"
DS_REVENUE = "kommo.revenue_mtd"
DS_LEADERBOARD = "kommo.leaderboard"
def geckoboard_session() -> requests.Session:
"""HTTP session с Basic Auth для Geckoboard."""
s = requests.Session()
s.auth = (GECKOBOARD_API_KEY, "") # password - пустая строка
s.headers.update({"Content-Type": "application/json"})
return s
def kommo_get(path: str, params: dict = None) -> dict:
"""GET запрос к Kommo API с авторизацией."""
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]:
"""
Получить все сделки с пагинацией.
Kommo API возвращает максимум 250 записей за запрос.
"""
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: # Последняя страница
break
return leads
def get_pipeline_stages() -> dict[int, str]:
"""Получить маппинг status_id -> название этапа."""
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]:
"""Агрегировать сделки по этапам: count и 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]:
"""Выручка текущего месяца: только Won сделки."""
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: # Ваш 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]:
"""
Leaderboard менеджеров: Won сделки за текущий месяц.
Используем responsible_user_id из сделки.
"""
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))
# Получить имена менеджеров
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]):
"""
Создать или обновить dataset в Geckoboard.
PUT /datasets/{id} - создаёт schema.
PUT /datasets/{id}/data - заменяет все данные.
"""
# 1. Убедиться что dataset существует (создать если нет)
resp = session.put(
f"{GECKOBOARD_BASE}/datasets/{dataset_id}",
json={"fields": schema}
)
resp.raise_for_status()
# 2. Заменить данные
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():
"""Основная ETL-функция: Kommo -> Geckoboard."""
logging.info("ETL run started")
gb_session = geckoboard_session()
try:
stages = get_pipeline_stages()
# Получить активные сделки (не Won/Lost)
active_leads = get_all_leads(params={"filter[statuses][0][pipeline_id]": "your-pipeline-id"})
# Получить Won сделки текущего месяца для revenue и leaderboard
all_leads = get_all_leads() # В реальном проекте добавьте фильтр по дате
# 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__":
# Запуск по расписанию: каждые 15 минут
scheduler = BlockingScheduler()
scheduler.add_job(run_etl, "interval", minutes=15)
run_etl() # Первый запуск сразу
scheduler.start()
Пошаговая реализация
Шаг 1. Получить API Key Geckoboard
В Geckoboard: иконка профиля в правом верхнем углу -> Account -> прокрутить вниз до API Key. Ключ используется как username в Basic Auth. Документация Geckoboard Datasets API: developer.geckoboard.com.
Шаг 2. Определить схему datasets
Geckoboard поддерживает типы полей: string, number, money, percentage, date, datetime. Схема фиксируется при создании dataset через PUT /datasets/{id}. Если нужно изменить схему - удалите dataset через DELETE /datasets/{id} и создайте заново.
Шаг 3. Найти Won status_id в Kommo
Status_id для статуса Won уникален для каждого аккаунта Kommo. Получите его через GET /api/v4/leads/pipelines - в ответе будет массив statuses с полем type: "won". Используйте этот ID в функциях фильтрации.
Шаг 4. Развернуть ETL как сервис
Запустите Python-скрипт как systemd service или в Docker. APScheduler запускает ETL каждые 15 минут. Для production добавьте алерт при ошибке ETL (например, через Sentry или Slack webhook) - если скрипт упал, дашборд покажет устаревшие данные без предупреждения.
Шаг 5. Настроить виджеты в Geckoboard
В Geckoboard: Add widget -> Datasets -> выбрать ваш dataset -> выбрать тип виджета. Для pipeline_by_stage подходит Bar Chart или Column Chart. Для leaderboard - Leaderboard widget (поддерживает ранжирование). Для revenue_mtd - Line Chart с накоплением.
Реальный кейс с цифрами
B2B-команда продаж в Лондоне, 12 человек (4 SDR + 4 AE + Head of Sales + операционный стек). До интеграции: Head of Sales делал ежедневный отчёт из Kommo вручную в Google Sheets, рассылал по email. Данные обновлялись раз в день, командный TV-экран показывал статичный слайд с прошлого совещания.
После интеграции: TV-экран в офисе показывает live pipeline - какие сделки на каком этапе, revenue недели, leaderboard по AE. Обновление каждые 15 минут. Head of Sales перестал тратить 30-45 минут утром на сборку отчёта. Remote-команда получила shared link на дашборд Geckoboard - тот же экран доступен с любого устройства.
Поведенческий эффект: AE начали самостоятельно следить за своей позицией в leaderboard и актуализировать данные в Kommo без напоминаний. Качество данных в CRM улучшилось как побочный эффект прозрачности.
Для кого подходит
Интеграция Kommo + Geckoboard подходит B2B-командам продаж от 6 до 30 человек, для которых важна командная видимость pipeline без ежедневных отчётных встреч. Geckoboard особенно удобен для офисов с TV-экраном и для remote-команд с distributed visibility через shared dashboards.
Если вы уже используете другие BI-инструменты для аналитики продаж, сравните с Kommo + Metabase (SQL-запросы к данным CRM, исторические тренды) и Kommo + Grafana (time-series метрики, alerting). Geckoboard проще в настройке виджетов, но менее гибок в аналитике. Metabase и Grafana дают больше аналитической мощи, но требуют промежуточной базы данных.
Для кастомных интеграций Kommo этот тип задачи - один из стандартных: ETL из CRM в dashboard-инструмент с гибкой схемой метрик.
Часто задаваемые вопросы
Как Geckoboard аутентифицирует запросы к Datasets API?
Geckoboard использует HTTP Basic Auth: API Key как username, пустая строка как password. При использовании Python requests: session.auth = (GECKOBOARD_API_KEY, ""). API Key находится в аккаунте: иконка профиля -> Account -> API Key. Rate limit: 60 запросов в минуту на ключ, при превышении API возвращает 429. Документация: developer.geckoboard.com.
Как часто нужно обновлять данные и сколько это стоит по API?
Geckoboard Datasets API не тарифицирует по количеству запросов - стоимость фиксированная по тарифу аккаунта. Ограничение: 60 запросов в минуту. При обновлении 3-5 datasets каждые 15 минут вы делаете примерно 10-15 запросов за один ETL-цикл - это существенно ниже rate limit. Kommo API имеет собственный rate limit: 7 запросов в секунду на аккаунт, учитывайте это при пагинации по большим объёмам сделок.
Можно ли добавить данные из Kommo и других систем в один дашборд?
Да. Geckoboard позволяет создавать неограниченное количество datasets из разных источников и размещать виджеты на одном дашборде. Например: pipeline из Kommo + support tickets из Intercom + revenue из Stripe на одном TV-экране. Каждый dataset обновляется независимо своим ETL-скриптом.
Как настроить leaderboard по SDR отдельно от AE?
В Kommo можно разделить менеджеров по тегам, командам или pipeline. Если у вас несколько pipeline (один для SDR outbound, второй для AE closing) - фильтруйте сделки по pipeline_id при запросе к Kommo API. В Geckoboard создайте два отдельных dataset: kommo.leaderboard_sdr и kommo.leaderboard_ae, разместите два Leaderboard виджета рядом.
Что происходит с дашбордом если ETL-скрипт упал?
Geckoboard продолжает показывать последние загруженные данные. Виджеты не исчезают и не показывают ошибку автоматически - это может ввести команду в заблуждение, если данные устарели на несколько часов. Рекомендуется: добавить в ETL мониторинг с алертом при падении (Sentry, Slack webhook), а в Geckoboard использовать функцию “Last updated” timestamp на виджете через поле типа datetime.
Что дальше
Если ваша команда продаж принимает решения по данным которые обновляются раз в день или реже - live дашборд меняет рабочую культуру быстрее чем любой процесс.
Опишите задачу команде Exceltic.dev: какие метрики нужны на дашборде, сколько менеджеров в команде, нужен ли TV-экран или shared link. Оценим объём работ и предложим конкретную схему datasets.