Shortcut (formerly Clubhouse) is a project management tool for development teams: Stories, Epics, Iterations (sprints). It is used in tech companies as a Jira alternative with a simpler UX. For B2B SaaS, the Kommo + Shortcut integration solves a specific problem: when a deal is closed in Kommo (Closed Won), automatically create a Story in Shortcut for an onboarding or implementation task, populated with client data and deal details.
The Shortcut API uses the Shortcut-Token: {API_KEY} header (not Bearer). Core operations: POST /api/v3/stories - create a task, GET /api/v3/workflows - retrieve available workflow statuses, GET /api/v3/members - retrieve team members. Stories in Shortcut have a Workflow State, Labels, Owners, and an Estimate.
Story in Shortcut - the basic unit of work (analogous to an Issue in Jira). Linked to a Workflow that defines the allowed statuses (Backlog -> In Dev -> In Review -> Done).
Architecture: Closed Won -> Onboarding Story
Kommo: deal -> Closed Won
-> webhook leads.status.changed (status_id = CLOSED_WON)
-> Your server
Your server
-> Kommo API: fetch deal and contact data
-> Shortcut API: POST /api/v3/stories
{name: "Onboarding: {client}", project_id, workflow_state_id,
description: client + amount + manager,
labels: [{name: "onboarding"}], owner_ids: [manager]}
-> Kommo: write story_id + link as a note
Implementation
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"]) # workflow ID for stories
SC_BACKLOG_STATE = int(os.environ["SHORTCUT_BACKLOG_STATE_ID"]) # Backlog state ID
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:
# Mapping Kommo user_id -> Shortcut member UUID
# In production: store in 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:** {client_name}",
f"**Email:** {email}" if email else "",
f"**Deal:** {deal_name}",
f"**Amount:** ${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 created: #{story_id}. Onboarding: {contact.get('name', '')}. Link: {story_url}",
)
return jsonify({"status": "ok"}), 200
Getting the 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']})")
# Run once to retrieve state IDs
get_workflow_states()
Copy the relevant IDs into the environment variables SHORTCUT_WORKFLOW_ID and SHORTCUT_BACKLOG_STATE_ID.
Mapping Kommo Managers to Shortcut Members
# .env
KOMMO_TO_SHORTCUT_USERS="123456:a1b2c3d4-...,789012:e5f6g7h8-..."
Kommo user_id (from /api/v4/users) -> Shortcut member UUID (from /api/v3/members). The assignee on the Shortcut task corresponds to the responsible manager in Kommo.
Automatically Creating an Epic for Large Deals
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"Full implementation cycle for Kommo deal #{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},
)
Who This Is For
B2B SaaS companies and tech agencies where the development team uses Shortcut and sales uses Kommo. Particularly useful for onboarding: every new client becomes a Story with setup, integration, and training tasks. The manager immediately sees the link in Kommo, while the developer gets client context in Shortcut.
A similar integration for team task management is described for Kommo + Plane.
Frequently Asked Questions
How do I get a Shortcut API Token?
Shortcut Settings -> Account -> API Tokens -> Generate Token. Tokens do not expire automatically but can be revoked. Each integration should use a separate token so it can be revoked without affecting others.
Can I create multiple Stories from a single deal?
Yes. Add multiple POST /api/v3/stories calls with different workflow_state_id values and descriptions - for example “Onboarding Tech”, “Onboarding Training”, “Account Setup”. Group them into one Epic via epic_id. Write all story IDs into a single note in Kommo.
How do I update a Story in Shortcut when a deal status changes in Kommo?
Use PUT /api/v3/stories/{id} with a new workflow_state_id. Store the story_id in a custom field in Kommo (similar to kommo_lead_id in other integrations). If the connection is lost, check the field - if it is already populated, update the existing story rather than creating a new one.
Summary
Kommo + Shortcut - development tasks straight from the pipeline:
Shortcut-Tokenheader (not Bearer),POST /api/v3/stories- Get Workflow State IDs via
/api/v3/workflowsonce owner_ids- map Kommo user_id -> Shortcut member UUID via env- Epic for large deals:
POST /api/v3/epics+PUT story/{id} {epic_id} - Write the Story URL to a Kommo note for two-way access
If you need a Kommo integration with Shortcut or another PM tool, describe your requirements to the Exceltic.dev team.