Kommo + Productive: проекты и ресурсы из закрытой сделки
При интеграции Kommo и Productive через API выигранная сделка автоматически создаёт проект с правильным бюджетом, назначенной командой и сервисами - без участия PM. Данные переносятся напрямую из карточки сделки: название клиента, сумма, тип работ, ответственный менеджер.
Для digital-агентств это стандартная точка потерь. При закрытии сделки PM вручную открывает Productive, создаёт проект, копирует бюджет из Kommo, назначает команду. При 10-15 новых проектах в месяц это не просто неудобно - это систематические ошибки: неверный бюджет (перепутали net/gross), не та команда (PM не знал что дизайнер в отпуске), неверный тип сервиса. Ниже - архитектура автоматической передачи данных и рабочий код.
Почему нативной интеграции нет и почему Zapier не подходит
Productive - project management и resource planning платформа, популярная среди digital-агентств в EU и US. Её документация доступна на developer.productive.io. Kommo не имеет нативного коннектора к Productive. Zapier предлагает базовый модуль для Productive, но он не поддерживает создание budgets и service assignments - только создание проектов без финансовой части.
Проблема в том, что Productive имеет связанные объекты: Project -> Budget -> Services -> Assignments. Создать проект через Zapier можно, но привязать бюджет с правильной суммой из Kommo и назначить team members на конкретные сервисы через Zapier невозможно без кастомного кода. Именно поэтому агентства либо делают всё вручную, либо пишут интеграцию самостоятельно.
Архитектура интеграции Kommo + Productive
Стек: Python 3.11, Kommo Webhooks, Productive API v2, PostgreSQL для журнала.
Модель данных Productive, которую нужно понять перед разработкой:
Project- основной объект, содержит название, клиента (company_id), статусBudget- финансовый план проекта, привязан к Project. Содержитbudget_total,currency,billing_typeService- тип работ внутри бюджета (Design, Development, PM)ProjectAssignment- назначение конкретногоperson_idна проект
Kommo Custom Fields, которые нужно заполнить в карточке сделки до закрытия:
deal_value- сумма сделки (переносится вbudget_total)service_type- тип проекта (маппится на Service IDs в Productive)assigned_team- список ID сотрудников через запятую (маппится на persons в Productive)billing_type- fixed/hourly (передаётся в Budget)
Auth: Productive API использует Bearer token (Personal Access Token). Генерируется в Productive: Settings -> My Account -> API Access Tokens. Токен передаётся в заголовке Authorization: Bearer {token}.
import logging
import requests
from flask import Flask, request, jsonify
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
PRODUCTIVE_TOKEN = "your-productive-pat"
PRODUCTIVE_BASE = "https://api.productive.io/api/v2"
PRODUCTIVE_ORG_ID = "your-org-id" # Из URL аккаунта Productive
KOMMO_BASE = "https://your-domain.kommo.com"
KOMMO_TOKEN = "your-kommo-token"
# Маппинг типов сервисов из Kommo custom field на Productive service_type_ids
SERVICE_MAP = {
"website": "111", # Service Type ID в Productive
"branding": "112",
"seo": "113",
"development": "114",
}
# Маппинг team members: email из Kommo -> person_id в Productive
TEAM_MAP = {
"pm@agency.com": "2001",
"design@agency.com": "2002",
"dev@agency.com": "2003",
}
def get_productive_headers() -> dict:
return {
"Authorization": f"Bearer {PRODUCTIVE_TOKEN}",
"Content-Type": "application/vnd.api+json",
"X-Organization-Id": PRODUCTIVE_ORG_ID,
}
def get_kommo_lead(lead_id: int) -> dict:
url = f"{KOMMO_BASE}/api/v4/leads/{lead_id}?with=contacts,custom_fields_values"
headers = {"Authorization": f"Bearer {KOMMO_TOKEN}"}
resp = requests.get(url, headers=headers, timeout=10)
resp.raise_for_status()
return resp.json()
def find_or_create_company(company_name: str) -> str:
"""Найти или создать Company в Productive для клиента."""
search_url = f"{PRODUCTIVE_BASE}/companies?filter[name]={company_name}"
resp = requests.get(search_url, headers=get_productive_headers(), timeout=10)
resp.raise_for_status()
companies = resp.json().get("data", [])
if companies:
return companies[0]["id"]
# Создать новую компанию
payload = {
"data": {
"type": "companies",
"attributes": {"name": company_name}
}
}
create_resp = requests.post(
f"{PRODUCTIVE_BASE}/companies",
json=payload,
headers=get_productive_headers(),
timeout=10
)
create_resp.raise_for_status()
return create_resp.json()["data"]["id"]
def create_project(name: str, company_id: str) -> str:
"""Создать проект в Productive."""
payload = {
"data": {
"type": "projects",
"attributes": {
"name": name,
"project_color": "#4A90D9",
},
"relationships": {
"company": {
"data": {"type": "companies", "id": company_id}
}
}
}
}
resp = requests.post(
f"{PRODUCTIVE_BASE}/projects",
json=payload,
headers=get_productive_headers(),
timeout=10
)
resp.raise_for_status()
project_id = resp.json()["data"]["id"]
logging.info(f"Created project {project_id}: {name}")
return project_id
def create_budget(project_id: str, total: float, currency: str, billing_type: str) -> str:
"""
Создать бюджет для проекта.
billing_type: 'fixed_price' | 'hourly'
"""
payload = {
"data": {
"type": "budgets",
"attributes": {
"name": "Project Budget",
"budget_total": total,
"currency_id": currency, # e.g. 'EUR', 'USD'
"billing_type": billing_type,
},
"relationships": {
"project": {
"data": {"type": "projects", "id": project_id}
}
}
}
}
resp = requests.post(
f"{PRODUCTIVE_BASE}/budgets",
json=payload,
headers=get_productive_headers(),
timeout=10
)
resp.raise_for_status()
budget_id = resp.json()["data"]["id"]
logging.info(f"Created budget {budget_id} for project {project_id}")
return budget_id
def assign_people_to_project(project_id: str, person_ids: list[str]):
"""Назначить team members на проект."""
for person_id in person_ids:
payload = {
"data": {
"type": "project_assignments",
"relationships": {
"project": {"data": {"type": "projects", "id": project_id}},
"person": {"data": {"type": "people", "id": person_id}}
}
}
}
resp = requests.post(
f"{PRODUCTIVE_BASE}/project_assignments",
json=payload,
headers=get_productive_headers(),
timeout=10
)
if resp.status_code == 409: # Already assigned
logging.info(f"Person {person_id} already assigned to project {project_id}")
continue
resp.raise_for_status()
logging.info(f"Assigned person {person_id} to project {project_id}")
@app.route("/kommo/webhook", methods=["POST"])
def kommo_webhook():
"""Webhook от Kommo при выигрыше сделки (status = won)."""
data = request.json
leads = data.get("leads", {}).get("status", [])
for lead_event in leads:
# Kommo status_id 142 для 'Won' может отличаться в вашем аккаунте
# Проверяйте через API: GET /api/v4/leads/pipelines
if lead_event.get("status_id") not in [142, 143]: # Ваши Won status IDs
continue
lead_id = lead_event["id"]
try:
lead = get_kommo_lead(lead_id)
# Извлечь custom fields
cf_values = {}
for cf in lead.get("custom_fields_values", []):
cf_values[cf["field_code"]] = cf["values"][0]["value"]
deal_name = lead.get("name", f"Deal {lead_id}")
deal_value = float(lead.get("price", 0))
company_name = cf_values.get("COMPANY", "Unknown Client")
service_type = cf_values.get("SERVICE_TYPE", "website")
billing_type = cf_values.get("BILLING_TYPE", "fixed_price")
team_emails = cf_values.get("ASSIGNED_TEAM", "").split(",")
# Маппинг team members
person_ids = [
TEAM_MAP[email.strip()]
for email in team_emails
if email.strip() in TEAM_MAP
]
# Создать объекты в Productive
company_id = find_or_create_company(company_name)
project_id = create_project(deal_name, company_id)
create_budget(project_id, deal_value, "EUR", billing_type)
assign_people_to_project(project_id, person_ids)
logging.info(
f"Lead {lead_id} -> Project {project_id} created in Productive. "
f"Budget: {deal_value} EUR. Team: {person_ids}"
)
except requests.HTTPError as e:
logging.error(f"HTTP error for lead {lead_id}: {e.response.status_code} {e.response.text}")
except Exception as e:
logging.exception(f"Error processing lead {lead_id}: {e}")
return jsonify({"status": "ok"})
@app.route("/productive/webhook", methods=["POST"])
def productive_webhook():
"""
Webhook от Productive при завершении задачи (task.completed)
или превышении бюджета (budget.exceeded).
"""
data = request.json
event_type = data.get("data", {}).get("type")
if event_type == "budget_notifications":
attrs = data["data"].get("attributes", {})
project_id = data["data"].get("relationships", {}).get("project", {}).get("data", {}).get("id")
logging.warning(f"Budget threshold exceeded for project {project_id}: {attrs}")
# Здесь: отправить уведомление в Slack или обновить заметку в Kommo
return jsonify({"status": "ok"})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)
Пошаговая реализация
Шаг 1. Подготовка Custom Fields в Kommo
До интеграции убедитесь что в карточке сделки есть поля: SERVICE_TYPE (select), BILLING_TYPE (select: fixed_price/hourly), ASSIGNED_TEAM (text, emails через запятую). Без этих данных проект будет создан с дефолтными значениями. Настройку воронки и custom fields разбирает статья о настройке воронки Kommo.
Шаг 2. Получение Organization ID в Productive
Organization ID виден в URL вашего Productive-аккаунта: app.productive.io/organizations/{ORG_ID}/.... Он нужен в заголовке каждого запроса как X-Organization-Id.
Шаг 3. Маппинг Service Types
Service Type IDs в Productive получаете через GET /api/v2/service_types. Создайте словарь SERVICE_MAP с маппингом ваших названий из Kommo на IDs Productive. Это однократная настройка.
Шаг 4. Настройка webhook Kommo
Kommo -> Settings -> Webhooks. URL: https://your-server.com/kommo/webhook. Событие: Lead status changed. Проверьте status_id для статуса Won в вашем аккаунте через GET /api/v4/leads/pipelines - он уникален для каждого аккаунта.
Шаг 5. Настройка webhooks Productive
Productive поддерживает webhooks для событий task.completed и бюджетных уведомлений. Настраивается в Settings -> Integrations -> Webhooks. Используйте для обратной нотификации: когда проект завершён, можно обновить статус сделки в Kommo или отправить уведомление.
Реальный кейс с цифрами
Digital-агентство в Берлине, 22 сотрудника, 12-18 новых проектов в месяц. До интеграции: PM тратил 40-60 минут на создание каждого проекта в Productive после закрытия сделки. За месяц - до 18 часов только на ручной перенос данных. Ошибки в бюджете: 2-3 проекта в месяц стартовали с неверной суммой из-за ручного ввода.
После запуска интеграции: проект в Productive создаётся через 10-15 секунд после смены статуса в Kommo. PM получает уведомление в Slack (через отдельный webhook) что проект готов. Ошибки в бюджете - 0 за первые три месяца работы. Экономия: ~15 часов PM в месяц, которые ушли на реальное планирование, а не перепечатку данных.
Дополнительный эффект: поскольку budget_total берётся напрямую из price сделки в Kommo, согласование «сколько мы продали» vs «сколько мы запланировали» стало мгновенным - данные всегда идентичны.
Для кого подходит
Интеграция Kommo + Productive максимально полезна для digital-агентств и сервисных компаний с 10-30 сотрудниками, которые ведут 8-20 новых проектов в месяц. Условие: Productive уже используется как основной инструмент project management и resource planning. Если вы только оцениваете варианты для задач из выигранных сделок - сравните с Kommo + Asana и Kommo + Wrike: каждый инструмент имеет свою модель данных и разный уровень детализации бюджетирования.
Productive отличается тем, что resource planning и financial tracking - его core feature, а не дополнение. Если для вашего агентства бюджет и загрузка команды важнее kanban-досок - Productive + Kommo через API решает эту задачу чище чем любой no-code коннектор. Подробнее о подходе к кастомным интеграциям Kommo.
Часто задаваемые вопросы
Как Productive API аутентифицирует запросы?
Productive API v2 использует Personal Access Token (PAT) в заголовке Authorization: Bearer {token}. Токен генерируется в настройках аккаунта: Settings -> My Account -> API Access Tokens. Дополнительно все запросы должны содержать заголовок X-Organization-Id с ID вашей организации в Productive (виден в URL аккаунта). Документация: developer.productive.io.
Можно ли создать проект из шаблона в Productive через API?
Да. Productive API поддерживает endpoint POST /api/v2/projects/copy с параметром source_project_id. Это позволяет скопировать шаблонный проект со стандартными задачами, milestone’ами и структурой сервисов. При копировании можно передать copy_budgets: true и copy_assignees: false чтобы сохранить финансовую структуру но назначить новую команду под конкретную сделку.
Что делать если person_id из Kommo не совпадает с Productive?
Productive и Kommo - отдельные системы с независимыми ID пользователей. Маппинг нужно создать вручную один раз: получить список людей из Productive через GET /api/v2/people и создать словарь email -> person_id. Обновлять маппинг нужно только при найме новых сотрудников. Храните маппинг в конфигурации или базе данных, не в коде.
Как обрабатывать превышение бюджета?
Productive отправляет webhook-уведомление при достижении порогов бюджета (настраивается в Budget Settings). Ваш сервер получает событие и может: отправить Slack-уведомление PM, добавить заметку в карточку сделки в Kommo, обновить custom field с текущим статусом бюджета. Это позволяет руководителю продаж видеть финансовое состояние проекта прямо в Kommo без переключения в Productive.
Как передать ставки сотрудников в бюджет Productive?
Productive поддерживает Service Assignments с почасовыми ставками. После создания бюджета создайте Services через POST /api/v2/services с указанием service_type_id и hourly_rate. Затем через POST /api/v2/service_assignments привяжите конкретных людей к сервисам. Ставки можно хранить в Kommo custom fields или в конфигурации интеграции - в зависимости от того, меняются ли они от проекта к проекту.
Что дальше
Если ваше агентство теряет 10+ часов в месяц на перенос данных из Kommo в Productive - это типичная задача для нас. Один раз настроенная интеграция работает без участия команды.
Опишите ваш стек команде Exceltic.dev: как устроена воронка в Kommo, какие custom fields заполняются при закрытии сделки и как организована структура проектов в Productive. Оценим объём работ и предложим архитектуру.