Kommo + Teamwork: Creating Agency Projects from Won Deals

Teamwork is a PM platform built specifically for agencies and consulting: projects, tasks, milestones, time tracking with client billing, resource planning, and a client portal. Unlike Asana or Monday.com, Teamwork has built-in time tracking with hourly billing — critical for agencies working on a Time & Materials model. Without a Kommo integration, creating a project on Won is a manual process. With the integration, Won -> project from a template with tasks in seconds.

Teamwork vs Asana vs Basecamp for Agency Delivery

ParameterTeamworkAsanaBasecamp
Time tracking + billingNative, billable/non-billableVia integrationNone
Client portalYesNoLimited
Project templatesYesYesYes
Resource planningYesYes (Business+)No
RetrospectivesVia ReportsNoNo
Best forT&M agencies, consultingAll typesSimple projects

Teamwork is chosen by agencies where every hour is billable and the client tracks progress through the client portal.

What Gets Synchronized

Kommo -> Teamwork:
— Won -> create a Project from a template with client data
— Won -> add a Description with deal data (plan, amount, manager)
— Won -> add the client as a Company in Teamwork
— Won -> assign tasks to the team via People mapping

Teamwork -> Kommo:
task.completed for a milestone task -> Note in the deal
project.completed -> Note + stage change to “Project Completed”
— Logged time reaches the budget -> Note: “80% of budget used”

Teamwork API: Key Requests

Base URL: https://{yourdomain}.teamwork.com/projects/api/v3.
Authentication: Basic Auth — API key as username, any password.
API Key: Teamwork -> Profile Settings -> API Keys.

import requests
from requests.auth import HTTPBasicAuth

TW_API_KEY   = "your_api_key"
TW_DOMAIN    = "yourcompany"  # yourcompany.teamwork.com
TW_BASE_URL  = f"https://{TW_DOMAIN}.teamwork.com/projects/api/v3"
TW_AUTH      = HTTPBasicAuth(TW_API_KEY, "x")  # any password

def get_project_templates() -> list:
    resp = requests.get(
        f"{TW_BASE_URL}/projecttemplates.json",
        auth=TW_AUTH,
    )
    resp.raise_for_status()
    return resp.json().get("projecttemplates", [])

def create_project_from_template(template_id: int, name: str,
                                  description: str = "") -> dict:
    payload = {
        "project": {
            "name": name,
            "description": description,
        }
    }
    resp = requests.post(
        f"{TW_BASE_URL}/projecttemplates/{template_id}/projects.json",
        auth=TW_AUTH,
        json=payload,
    )
    resp.raise_for_status()
    return resp.json().get("project", {})

def create_project(name: str, description: str = "") -> dict:
    payload = {
        "project": {
            "name": name,
            "description": description,
        }
    }
    resp = requests.post(
        f"{TW_BASE_URL}/projects.json",
        auth=TW_AUTH,
        json=payload,
    )
    resp.raise_for_status()
    return resp.json().get("project", {})

def get_tasklists(project_id: int) -> list:
    resp = requests.get(
        f"{TW_BASE_URL}/projects/{project_id}/tasklists.json",
        auth=TW_AUTH,
    )
    resp.raise_for_status()
    return resp.json().get("tasklists", [])

def create_tasklist(project_id: int, name: str) -> dict:
    payload = {"tasklist": {"name": name}}
    resp = requests.post(
        f"{TW_BASE_URL}/projects/{project_id}/tasklists.json",
        auth=TW_AUTH,
        json=payload,
    )
    resp.raise_for_status()
    return resp.json().get("tasklist", {})

def create_task(project_id: int, tasklist_id: int, content: str,
                assignee_id: int = None, due_date: str = None) -> dict:
    # due_date: "YYYYMMDD"
    payload: dict = {
        "todo-item": {
            "content": content,
        }
    }
    if assignee_id:
        payload["todo-item"]["responsible-party-id"] = str(assignee_id)
    if due_date:
        payload["todo-item"]["due-date"] = due_date

    resp = requests.post(
        f"{TW_BASE_URL}/tasklists/{tasklist_id}/tasks.json",
        auth=TW_AUTH,
        json=payload,
    )
    resp.raise_for_status()
    return resp.json().get("todo-item", {})

ONBOARDING_TASKS = [
    "Kick-off call with the client team",
    "Requirements gathering and brief",
    "Roadmap sign-off",
    "First delivery review",
    "Final acceptance and closure",
]

MANAGER_TO_TW = {
    "alice@company.com": 100001,
    "bob@company.com":   100002,
}

def on_deal_won(lead: dict, contact: dict):
    client_name = contact["name"]
    plan        = get_custom_field(lead, PLAN_FIELD_ID) or "Growth"
    amount      = lead.get("price", 0)
    manager_email = get_manager_email(lead)

    description = (
        f"Client: {client_name}\n"
        f"Plan: {plan}\n"
        f"Amount: ${amount}\n"
        f"Kommo deal: {lead['id']}"
    )
    project_name = f"{client_name} - {plan}"

    if TW_TEMPLATE_ID:
        project = create_project_from_template(TW_TEMPLATE_ID, project_name, description)
    else:
        project = create_project(project_name, description)

    project_id = project["id"]
    tasklists  = get_tasklists(project_id)

    if not tasklists:
        tl = create_tasklist(project_id, "Onboarding")
        tasklist_id = tl["id"]
    else:
        tasklist_id = tasklists[0]["id"]

    assignee_id = MANAGER_TO_TW.get(manager_email)
    for task_name in ONBOARDING_TASKS:
        create_task(project_id, tasklist_id, task_name, assignee_id)

    update_kommo_deal(lead["id"], {"teamwork_project_id": str(project_id)})
    create_kommo_note(lead["id"],
        f"Teamwork: project '{project_name}' created (ID: {project_id})")

Teamwork Webhook:

@app.route("/webhooks/teamwork", methods=["POST"])
def teamwork_webhook():
    payload   = request.json
    event     = payload.get("eventName")
    project   = payload.get("project", {})
    project_id = str(project.get("id", ""))

    deal_id = find_deal_by_field("teamwork_project_id", project_id)
    if not deal_id:
        return "", 200

    if event == "TASK.COMPLETED":
        task_name = payload.get("task", {}).get("name", "")
        create_kommo_note(deal_id,
            f"Teamwork: task '{task_name}' completed")

    elif event == "PROJECT.COMPLETED":
        update_kommo_deal(deal_id, {"stage_id": STAGE_PROJECT_DONE})
        create_kommo_note(deal_id, "Teamwork: project completed")

    return "", 200

Time Tracking: Billing Cycle via Kommo

For agencies on a T&M model, Teamwork makes it possible to see how many hours have been spent on a client directly from their deal card in Kommo. Polling the API GET /projects/{id}/time.json daily:

  • If billable_hours >= budget_hours × 0.8 -> Note in Kommo: “80% of project budget used”
  • The manager sees this in the card and initiates a conversation about scope expansion

This is not automated natively — custom logic is required, but polling the Teamwork Time API + Kommo Note is a standard task for such an integration.

Real-World Case

Digital agency (Ireland, Teamwork + Kommo, 15–20 projects/month):

  • Before: the project manager created a Teamwork project manually for every Won. 25–30 minutes for standard onboarding. Sometimes forgot to assign tasks to specific people -> tasks were left without an owner.
  • After: Won -> project from template with 5 tasks in 20 seconds. Owners assigned automatically via manager -> Teamwork ID mapping. The PM receives a Teamwork notification rather than creating the project themselves.
  • Additionally: PROJECT.COMPLETED -> stage change in Kommo -> trigger for an NPS survey via GetResponse.

Who This Is Relevant For

  • Agencies and consulting firms using Teamwork as their primary PM tool
  • Companies on Time & Materials with hourly client billing
  • Teams of 5–30 people with 10–30 active clients in parallel
  • Agencies where the client portal is important — the client tracks progress in Teamwork

Frequently Asked Questions

Teamwork API v1 vs v3 — which to use?

v3 — the current version with REST architecture. v1 (deprecated) uses a different notation and is no longer developed. For new integrations: always use v3 at /{domain}.teamwork.com/projects/api/v3.

How to create Teamwork project templates?

Teamwork -> More -> Templates -> New Template. Add the tasklists and tasks needed for every new project. Template ID — from the URL when editing: /templates/{id}/edit. Via API: GET /projecttemplates.json.

How does Teamwork authenticate webhooks?

Teamwork signs webhooks via HMAC-SHA256. A Webhook Secret field is available in the webhook settings. Verification: hmac.new(secret, payload, sha256).hexdigest() compared against the X-Webhook-Signature header.

How to configure Teamwork Client Portal access for a client?

Teamwork -> project -> People -> Add Client. The client receives an email invitation. In the portal, the client only sees the permitted tasklists and tasks (not everything). For the Kommo integration: when creating a project via the API, the client email can be added immediately via POST /projects/{id}/people.json.

Summary

  • Teamwork API: Basic Auth (api_key + “x”), /{domain}.teamwork.com/projects/api/v3
  • Create from template: POST /projecttemplates/{id}/projects.json
  • Tasks: POST /tasklists/{id}/tasks.json with responsible-party-id
  • Webhook: TASK.COMPLETED, PROJECT.COMPLETED, HMAC-SHA256
  • Time tracking: polling GET /projects/{id}/time.json -> Note when approaching the budget

If you use Teamwork and Kommo and want to automate project creation on Won — describe your template structure and manager mapping. Exceltic.dev will configure the integration.

More articles

All →