Kommo + Rocketlane: Automatic Client Onboarding from Won Deals

Why the native integration doesn’t work

Rocketlane is a platform for client onboarding and project delivery. Its main differentiator from Jira or Asana is the customer portal: the client can see their onboarding progress in real time, communicate with the team in the context of tasks, and upload documents. This reduces the load on CSMs (Customer Success Managers) and creates a transparent process for the client.

There is no native Rocketlane + Kommo integration. A typical problem: a deal closes in Kommo, the salesperson announces in Slack “client Acme Corp signed the contract,” the CSM manually creates a project in Rocketlane, enters client data, adds members. It takes 2-3 days for actual onboarding to begin. The client receives no information during this period.

Connecting custom Kommo integrations with the Rocketlane API solves this: the project is created automatically the moment the deal closes.

What gets built - solution architecture

Kommo: deal moves to Won status
    --> Webhook --> Python service
        --> Rocketlane API: POST /projects (from template_id)
        --> Rocketlane API: POST /projects/{id}/members (add client)
        --> Kommo API: add Note about created project

Rocketlane: milestone.completed
    --> Webhook --> Python service
        --> Kommo API: add Note to deal

Technical details

Rocketlane API Auth. Bearer token. Obtained in Rocketlane Settings -> API -> Generate Token. Passed in the Authorization: Bearer {token} header.

Rocketlane API endpoints:

  • POST /v1/projects - create a project. Parameters: name, templateId, startDate, description
  • GET /v1/templates - list project templates
  • POST /v1/projects/{id}/members - add a member. Parameters: email, role (customer/team)
  • GET /v1/projects/{id}/milestones - list milestones
  • Webhooks: configured in Rocketlane Settings -> Integrations -> Webhooks

Won status in Kommo. In Kommo, each pipeline has a special “Won” status. Its ID can be found via GET /api/v4/pipelines/{pipeline_id}/statuses. The Won status has is_final: true and type: won.

Rocketlane Customer Portal. When adding a member with role: customer, Rocketlane automatically sends an email invitation to the portal. The client receives a personal link and sees the onboarding plan.

Step-by-step implementation

Step 1. Identify Won status

import os
import requests

KOMMO_DOMAIN = os.environ["KOMMO_DOMAIN"]
KOMMO_TOKEN = os.environ["KOMMO_ACCESS_TOKEN"]
ROCKETLANE_TOKEN = os.environ["ROCKETLANE_TOKEN"]
ROCKETLANE_BASE = "https://api.rocketlane.com/api/v1"


def get_won_status_ids() -> set[int]:
    """Get all Won status IDs from all Kommo pipelines."""
    url = f"https://{KOMMO_DOMAIN}/api/v4/pipelines"
    headers = {"Authorization": f"Bearer {KOMMO_TOKEN}"}
    r = requests.get(url, params={"with": "statuses"}, headers=headers, timeout=10)
    if not r.ok:
        return set()

    won_ids = set()
    pipelines = r.json().get("_embedded", {}).get("pipelines", [])
    for pipeline in pipelines:
        statuses = pipeline.get("_embedded", {}).get("statuses", [])
        for status in statuses:
            if status.get("type") == 142:  # 142 = Won in Kommo
                won_ids.add(status["id"])
    return won_ids

Step 2. Create Rocketlane project from template

from flask import Flask, request
from datetime import datetime, timedelta

app = Flask(__name__)

ROCKETLANE_TEMPLATE_ID = os.environ.get("ROCKETLANE_TEMPLATE_ID", "")
# Cache Won statuses at startup
WON_STATUS_IDS = get_won_status_ids()


def get_lead_data(lead_id: int) -> dict:
    """Get deal data including contacts and company."""
    url = f"https://{KOMMO_DOMAIN}/api/v4/leads/{lead_id}"
    headers = {"Authorization": f"Bearer {KOMMO_TOKEN}"}
    r = requests.get(url, params={"with": "contacts,companies"}, headers=headers, timeout=10)
    r.raise_for_status()
    return r.json()


def get_contact_email_and_name(contact_id: int) -> tuple[str, str]:
    """Get contact email and name."""
    url = f"https://{KOMMO_DOMAIN}/api/v4/contacts/{contact_id}"
    headers = {"Authorization": f"Bearer {KOMMO_TOKEN}"}
    r = requests.get(url, headers=headers, timeout=10)
    if not r.ok:
        return "", ""

    contact = r.json()
    name = contact.get("name", "")
    email = ""
    for cf in contact.get("custom_fields_values", []) or []:
        if cf.get("field_code") == "EMAIL":
            email = cf["values"][0]["value"]
            break
    return email, name


def create_rocketlane_project(project_name: str, template_id: str) -> dict | None:
    """Create a project in Rocketlane from a template."""
    url = f"{ROCKETLANE_BASE}/projects"
    headers = {
        "Authorization": f"Bearer {ROCKETLANE_TOKEN}",
        "Content-Type": "application/json",
    }
    start_date = (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d")
    payload = {
        "name": project_name,
        "templateId": template_id,
        "startDate": start_date,
        "status": "active",
    }
    r = requests.post(url, json=payload, headers=headers, timeout=30)
    if not r.ok:
        print(f"Rocketlane project creation failed: {r.status_code} {r.text}")
        return None
    return r.json()


def add_customer_to_project(project_id: str, email: str, name: str) -> bool:
    """Add a client to a Rocketlane project (they will receive a portal invitation)."""
    url = f"{ROCKETLANE_BASE}/projects/{project_id}/members"
    headers = {
        "Authorization": f"Bearer {ROCKETLANE_TOKEN}",
        "Content-Type": "application/json",
    }
    first, *last_parts = name.split(" ", 1)
    payload = {
        "email": email,
        "role": "customer",
        "firstName": first,
        "lastName": last_parts[0] if last_parts else "",
    }
    r = requests.post(url, json=payload, headers=headers, timeout=10)
    return r.ok


def add_kommo_note(lead_id: int, text: str):
    url = f"https://{KOMMO_DOMAIN}/api/v4/leads/{lead_id}/notes"
    headers = {
        "Authorization": f"Bearer {KOMMO_TOKEN}",
        "Content-Type": "application/json",
    }
    payload = [{"note_type": "common", "params": {"text": text}}]
    requests.post(url, json=payload, headers=headers, timeout=10)


@app.route("/webhooks/kommo/won", methods=["POST"])
def kommo_won_webhook():
    """Handle deal transition to Won."""
    data = request.form
    lead_id = int(data.get("leads[status][0][id]", 0))
    new_status_id = int(data.get("leads[status][0][status_id]", 0))

    if not lead_id or new_status_id not in WON_STATUS_IDS:
        return {"ok": True}

    lead = get_lead_data(lead_id)
    lead_name = lead.get("name", f"Deal #{lead_id}")

    # Get primary contact data
    contacts = lead.get("_embedded", {}).get("contacts", [])
    if not contacts:
        add_kommo_note(lead_id, "Rocketlane: could not create project - no contact in deal")
        return {"ok": True}

    contact_id = contacts[0]["id"]
    email, name = get_contact_email_and_name(contact_id)

    if not email:
        add_kommo_note(lead_id, "Rocketlane: could not create project - no email for contact")
        return {"ok": True}

    # Get company name
    companies = lead.get("_embedded", {}).get("companies", [])
    company_name = companies[0].get("name", "") if companies else ""
    project_name = f"Onboarding: {company_name or name}"

    # Create project
    project = create_rocketlane_project(project_name, ROCKETLANE_TEMPLATE_ID)
    if not project:
        add_kommo_note(lead_id, "Rocketlane: error creating project")
        return {"ok": True}

    project_id = project["id"]
    project_url = project.get("portalUrl", "")

    # Add client
    add_customer_to_project(project_id, email, name)

    # Record in Kommo
    note_text = (
        f"Rocketlane: onboarding project created\n"
        f"Project: {project_name}\n"
        f"ID: {project_id}\n"
        f"Portal: {project_url}\n"
        f"Client added: {email}"
    )
    add_kommo_note(lead_id, note_text)

    # Save project ID to custom Kommo field
    ROCKETLANE_FIELD_ID = int(os.environ.get("KOMMO_ROCKETLANE_FIELD_ID", 0))
    if ROCKETLANE_FIELD_ID:
        patch_url = f"https://{KOMMO_DOMAIN}/api/v4/leads"
        headers = {
            "Authorization": f"Bearer {KOMMO_TOKEN}",
            "Content-Type": "application/json",
        }
        requests.patch(patch_url, json=[{
            "id": lead_id,
            "custom_fields_values": [{
                "field_id": ROCKETLANE_FIELD_ID,
                "values": [{"value": project_id}]
            }]
        }], headers=headers, timeout=10)

    return {"ok": True}


@app.route("/webhooks/rocketlane", methods=["POST"])
def rocketlane_webhook():
    """Handle Rocketlane events (milestone completed)."""
    event = request.json
    event_type = event.get("eventType")

    if event_type != "milestone.completed":
        return {"ok": True}

    project_id = event.get("projectId", "")
    milestone_name = event.get("milestoneName", "")

    # Find deal by project_id from custom field
    lead_id = find_lead_by_rocketlane_project(project_id)
    if lead_id:
        add_kommo_note(
            lead_id,
            f"Rocketlane: milestone completed - '{milestone_name}'"
        )

    return {"ok": True}


def find_lead_by_rocketlane_project(project_id: str) -> int | None:
    """Find a deal in Kommo by the Rocketlane Project ID custom field."""
    ROCKETLANE_FIELD_ID = int(os.environ.get("KOMMO_ROCKETLANE_FIELD_ID", 0))
    if not ROCKETLANE_FIELD_ID:
        return None
    url = f"https://{KOMMO_DOMAIN}/api/v4/leads"
    headers = {"Authorization": f"Bearer {KOMMO_TOKEN}"}
    params = {f"filter[custom_fields][{ROCKETLANE_FIELD_ID}][]": project_id}
    r = requests.get(url, params=params, headers=headers, timeout=10)
    if r.ok:
        leads = r.json().get("_embedded", {}).get("leads", [])
        return leads[0]["id"] if leads else None
    return None


if __name__ == "__main__":
    app.run(port=5000)

Real case with numbers

For a SaaS company with ARR above $500k and 5-10 new clients per month, automating onboarding through Rocketlane is critical for scaling the CSM team.

Before the integration: the CSM received a signal about a new client from a Slack notification from the salesperson. Creating a project in Rocketlane took 20-40 minutes: fill in the data, select a template, add the client, set timelines. With 8 new clients per month - 3-5 hours of CSM time just on project creation.

After the integration: the project is created automatically within 30 seconds of closing the deal. The client receives an invitation to the customer portal via email from Rocketlane before the CSM even opens a laptop. A typical outcome is reducing time-to-onboarding from 2-3 days to a few hours.

An additional effect: the CSM sees all client data in Rocketlane (company name, contact name, deal size) already filled in - no need to transfer manually from Kommo.

Who this is for

The integration is relevant for companies that:

  • Use Rocketlane for structured client onboarding with a customer portal
  • Track their sales pipeline in Kommo and want to automate the handoff from sales to success
  • Have a repeatable onboarding process (template in Rocketlane) applied to each new client
  • Work in B2B SaaS or professional services with an onboarding cycle of 2+ weeks

If you use other task management tools - for example, Kommo + Asana or Kommo + ClickUp - the Won-event automation principle is analogous.

Frequently asked questions

Can different Rocketlane templates be used for different pricing plans? Yes. Add a “Pricing Plan” custom field in Kommo. In the webhook handler, read this field and select the corresponding templateId from a mapping. For example: Plan_Basic -> template_001, Plan_Pro -> template_002.

What if the deal was marked Won retroactively and onboarding has already started? Add a duplicate check: before creating a project, check the rocketlane_project_id custom field in the deal. If it’s already filled - the project exists, skip creation.

Can multiple client contacts be added to Rocketlane? Yes. Kommo allows multiple contacts to be linked to a deal. In the handler, iterate over all contacts of type “client” and call add_customer_to_project for each.

Is reverse sync of all Rocketlane tasks to Kommo needed? Not recommended - it creates noise in Kommo. Syncing key milestones is sufficient (completion of onboarding, first client success). Minor Rocketlane tasks don’t need to be in the CRM.

If you need a Kommo + Rocketlane integration - describe your stack and scenario to the Exceltic.dev team. We’ll work through the architecture in one meeting.

More articles

All →