Kommo + Shortcut: Dev Tasks from Won Deals Without Manual Data Entry

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-Token header (not Bearer), POST /api/v3/stories
  • Get Workflow State IDs via /api/v3/workflows once
  • 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.

More articles

All →