Shortcut (ex-Clubhouse) - инструмент управления проектами для команд разработки: Stories, Epics, Iterations (спринты). Используется в tech-компаниях как альтернатива Jira с более простым UX. Для B2B SaaS интеграция Kommo + Shortcut решает конкретную проблему: при закрытии сделки в Kommo (Closed Won) автоматически создать Story в Shortcut для onboarding-задачи или внедрения, с данными клиента и деталями сделки.
Shortcut API использует заголовок Shortcut-Token: {API_KEY} (не Bearer). Основные операции: POST /api/v3/stories - создать задачу, GET /api/v3/workflows - получить доступные workflow-статусы, GET /api/v3/members - получить участников. Stories в Shortcut имеют Workflow State, Labels, Owners, Estimate.
Story в Shortcut - базовая единица работы (аналог Issue в Jira). Привязана к Workflow, который определяет допустимые статусы (Backlog -> In Dev -> In Review -> Done).
Архитектура: Closed Won -> Story для onboarding
Kommo: сделка -> Closed Won
-> webhook leads.status.changed (status_id = CLOSED_WON)
-> Ваш сервер
Ваш сервер
-> Kommo API: получить данные сделки и контакта
-> Shortcut API: POST /api/v3/stories
{name: "Onboarding: {client}", project_id, workflow_state_id,
description: клиент + сумма + менеджер,
labels: [{name: "onboarding"}], owner_ids: [менеджер]}
-> Kommo: записать story_id + ссылку как note
Реализация
import requests, os
from flask import Flask, request, jsonify
app = Flask(__name__)
SC_TOKEN = os.environ["SHORTCUT_API_TOKEN"]
SC_BASE = "https://api.app.shortcut.com/api/v3"
SC_HDR = {"Shortcut-Token": SC_TOKEN, "Content-Type": "application/json"}
KOMMO_SUBDOMAIN = os.environ["KOMMO_SUBDOMAIN"]
KOMMO_TOKEN = os.environ["KOMMO_ACCESS_TOKEN"]
CLOSED_WON_ID = int(os.environ["KOMMO_CLOSED_WON_ID"])
SC_WORKFLOW_ID = int(os.environ["SHORTCUT_WORKFLOW_ID"]) # ID workflow для stories
SC_BACKLOG_STATE = int(os.environ["SHORTCUT_BACKLOG_STATE_ID"]) # ID состояния Backlog
SC_DEFAULT_OWNER = os.environ.get("SHORTCUT_DEFAULT_OWNER", "") # member UUID
KOMMO_BASE = f"https://{KOMMO_SUBDOMAIN}.kommo.com/api/v4"
KOMMO_HDR = {"Authorization": f"Bearer {KOMMO_TOKEN}", "Content-Type": "application/json"}
def get_lead_details(lead_id: int) -> tuple[dict, dict]:
r = requests.get(
f"{KOMMO_BASE}/leads/{lead_id}",
headers=KOMMO_HDR,
params={"with": "contacts,custom_fields_values"},
)
lead = r.json()
contacts = lead.get("_embedded", {}).get("contacts", [])
contact = {}
if contacts:
rc = requests.get(
f"{KOMMO_BASE}/contacts/{contacts[0]['id']}",
headers=KOMMO_HDR,
params={"with": "custom_fields_values"},
)
contact = rc.json()
return lead, contact
def get_manager_shortcut_id(kommo_user_id: int) -> str | None:
# Маппинг Kommo user_id -> Shortcut member UUID
# В production: хранить в config/env
mapping_raw = os.environ.get("KOMMO_TO_SHORTCUT_USERS", "")
mapping = {}
for pair in mapping_raw.split(","):
parts = pair.split(":")
if len(parts) == 2:
mapping[parts[0].strip()] = parts[1].strip()
return mapping.get(str(kommo_user_id)) or (SC_DEFAULT_OWNER if SC_DEFAULT_OWNER else None)
def get_contact_email(contact: dict) -> str:
for cf in contact.get("custom_fields_values", []) or []:
if cf.get("field_code") == "EMAIL":
vals = cf.get("values", [])
if vals:
return vals[0].get("value", "")
return ""
def create_onboarding_story(lead: dict, contact: dict, lead_id: int) -> tuple[str, str]:
client_name = contact.get("name", f"Lead #{lead_id}")
deal_name = lead.get("name", "")
deal_amount = lead.get("price", 0) or 0
email = get_contact_email(contact)
responsible = lead.get("responsible_user_id")
owner_uuid = get_manager_shortcut_id(responsible) if responsible else SC_DEFAULT_OWNER
description_lines = [
f"**Клиент:** {client_name}",
f"**Email:** {email}" if email else "",
f"**Сделка:** {deal_name}",
f"**Сумма:** ${deal_amount:,}",
f"**Kommo Lead ID:** [{lead_id}](https://{os.environ['KOMMO_SUBDOMAIN']}.kommo.com/leads/detail/{lead_id})",
]
description = "
".join(line for line in description_lines if line)
payload = {
"name": f"Onboarding: {client_name}",
"description": description,
"story_type": "feature",
"workflow_state_id": SC_BACKLOG_STATE,
"labels": [{"name": "onboarding"}, {"name": "new-client"}],
}
if owner_uuid:
payload["owner_ids"] = [owner_uuid]
r = requests.post(f"{SC_BASE}/stories", headers=SC_HDR, json=payload)
r.raise_for_status()
story = r.json()
return str(story.get("id", "")), story.get("app_url", "")
def add_note(lead_id: int, text: str):
requests.post(
f"{KOMMO_BASE}/notes",
headers=KOMMO_HDR,
json=[{
"entity_id": lead_id,
"entity_type": "leads",
"note_type": "common",
"params": {"text": text},
}],
)
@app.route("/webhooks/kommo", methods=["POST"])
def kommo_webhook():
data = request.json or {}
for lead_data in data.get("leads", {}).get("status", []):
lead_id = lead_data.get("id")
new_status = lead_data.get("status_id")
if new_status != CLOSED_WON_ID:
continue
lead, contact = get_lead_details(lead_id)
story_id, story_url = create_onboarding_story(lead, contact, lead_id)
add_note(
lead_id,
f"Shortcut Story создана: #{story_id}. Onboarding: {contact.get('name', '')}. Ссылка: {story_url}",
)
return jsonify({"status": "ok"}), 200
Получение Workflow State ID
def get_workflow_states():
r = requests.get(f"{SC_BASE}/workflows", headers=SC_HDR)
for wf in r.json():
print(f"Workflow: {wf['name']} (ID: {wf['id']})")
for state in wf.get("states", []):
print(f" State: {state['name']} (ID: {state['id']})")
# Запустить один раз для получения ID состояний
get_workflow_states()
Скопируйте нужные ID в переменные окружения SHORTCUT_WORKFLOW_ID и SHORTCUT_BACKLOG_STATE_ID.
Маппинг менеджеров Kommo -> Shortcut
# .env
KOMMO_TO_SHORTCUT_USERS="123456:a1b2c3d4-...,789012:e5f6g7h8-..."
Kommo user_id (из /api/v4/users) -> Shortcut member UUID (из /api/v3/members). Задача assignee в Shortcut соответствует ответственному менеджеру в Kommo.
Автоматическое создание Epic при крупных сделках
def create_epic_for_large_deal(lead: dict, story_id: int, lead_id: int):
if (lead.get("price") or 0) < 10_000:
return
r = requests.post(
f"{SC_BASE}/epics",
headers=SC_HDR,
json={
"name": f"Implementation: {lead.get('name', '')}",
"description": f"Полный цикл внедрения для сделки Kommo #{lead_id}",
"labels": [{"name": "enterprise"}],
},
)
epic_id = r.json().get("id")
if epic_id:
requests.put(
f"{SC_BASE}/stories/{story_id}",
headers=SC_HDR,
json={"epic_id": epic_id},
)
Для кого актуально
B2B SaaS и tech-агентства, где команда разработки использует Shortcut, а продажи - Kommo. Особенно полезно для onboarding: каждый новый клиент = Story с задачами настройки, интеграции, обучения. Менеджер сразу видит ссылку в Kommo, разработчик получает контекст о клиенте в Shortcut.
Аналогичная интеграция для командных задач описана для Kommo + Plane.
Часто задаваемые вопросы
Как получить Shortcut API Token?
Shortcut Settings -> Account -> API Tokens -> Generate Token. Токен не истекает автоматически, но можно отозвать. Каждая интеграция должна использовать отдельный токен для возможности отзыва без влияния на другие.
Можно ли создавать несколько Stories из одной сделки?
Да. Добавьте несколько POST /api/v3/stories вызовов с разными workflow_state_id и описаниями - например “Onboarding Tech”, “Onboarding Training”, “Account Setup”. Объедините их в один Epic через epic_id. Все story_id запишите в одну note в Kommo.
Как обновить Story в Shortcut при изменении статуса сделки в Kommo?
PUT /api/v3/stories/{id} с новым workflow_state_id. Храните story_id в кастомном поле Kommo (аналогично kommo_lead_id в других интеграциях). При потере соединения проверяйте поле - если уже заполнено, обновляйте, а не создавайте новую.
Итог
Kommo + Shortcut - задачи разработки из воронки:
Shortcut-Tokenзаголовок (не Bearer),POST /api/v3/stories- Workflow State ID получить через
/api/v3/workflowsодин раз owner_ids- маппинг Kommo user_id -> Shortcut member UUID через env- Epic для крупных сделок:
POST /api/v3/epics+PUT story/{id} {epic_id} - Story URL записать в Kommo note для двустороннего доступа
Если нужна интеграция Kommo с Shortcut или другим PM-инструментом - опишите задачу команде Exceltic.dev.