Kommo + Productive: Projects and Resources from a Closed Deal
When Kommo and Productive are integrated via API, a won deal automatically creates a project with the correct budget, assigned team, and services - without PM involvement. Data is transferred directly from the deal card: client name, amount, type of work, responsible manager.
For digital agencies this is a standard point of loss. When a deal closes, the PM manually opens Productive, creates a project, copies the budget from Kommo, and assigns the team. With 10-15 new projects per month this is not just inconvenient - it leads to systematic errors: wrong budget (net/gross mix-up), wrong team (PM did not know the designer was on vacation), wrong service type. Below is the architecture for automatic data transfer and working code.
Why There Is No Native Integration and Why Zapier Does Not Work
Productive is a project management and resource planning platform popular among digital agencies in the EU and US. Its documentation is available at developer.productive.io. Kommo has no native connector to Productive. Zapier offers a basic Productive module, but it does not support creating budgets and service assignments - only project creation without the financial part.
The issue is that Productive has linked objects: Project -> Budget -> Services -> Assignments. You can create a project via Zapier, but attaching a budget with the correct amount from Kommo and assigning team members to specific services through Zapier is impossible without custom code. That is why agencies either do everything manually or build the integration themselves.
Kommo + Productive Integration Architecture
Stack: Python 3.11, Kommo Webhooks, Productive API v2, PostgreSQL for the log.
The Productive data model you need to understand before development:
Project- main object, contains name, client (company_id), statusBudget- financial plan for the project, linked to Project. Containsbudget_total,currency,billing_typeService- type of work within the budget (Design, Development, PM)ProjectAssignment- assignment of a specificperson_idto the project
Kommo Custom Fields that need to be filled in the deal card before closing:
deal_value- deal amount (transferred tobudget_total)service_type- project type (mapped to Service IDs in Productive)assigned_team- comma-separated list of employee IDs (mapped to persons in Productive)billing_type- fixed/hourly (passed to Budget)
Auth: Productive API uses a Bearer token (Personal Access Token). Generate it in Productive: Settings -> My Account -> API Access Tokens. The token is passed in the Authorization: Bearer {token} header.
import logging
import requests
from flask import Flask, request, jsonify
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
PRODUCTIVE_TOKEN = "your-productive-pat"
PRODUCTIVE_BASE = "https://api.productive.io/api/v2"
PRODUCTIVE_ORG_ID = "your-org-id" # From your Productive account URL
KOMMO_BASE = "https://your-domain.kommo.com"
KOMMO_TOKEN = "your-kommo-token"
# Mapping of service types from Kommo custom field to Productive service_type_ids
SERVICE_MAP = {
"website": "111", # Service Type ID in Productive
"branding": "112",
"seo": "113",
"development": "114",
}
# Mapping of team members: email from Kommo -> person_id in Productive
TEAM_MAP = {
"pm@agency.com": "2001",
"design@agency.com": "2002",
"dev@agency.com": "2003",
}
def get_productive_headers() -> dict:
return {
"Authorization": f"Bearer {PRODUCTIVE_TOKEN}",
"Content-Type": "application/vnd.api+json",
"X-Organization-Id": PRODUCTIVE_ORG_ID,
}
def get_kommo_lead(lead_id: int) -> dict:
url = f"{KOMMO_BASE}/api/v4/leads/{lead_id}?with=contacts,custom_fields_values"
headers = {"Authorization": f"Bearer {KOMMO_TOKEN}"}
resp = requests.get(url, headers=headers, timeout=10)
resp.raise_for_status()
return resp.json()
def find_or_create_company(company_name: str) -> str:
"""Find or create a Company in Productive for the client."""
search_url = f"{PRODUCTIVE_BASE}/companies?filter[name]={company_name}"
resp = requests.get(search_url, headers=get_productive_headers(), timeout=10)
resp.raise_for_status()
companies = resp.json().get("data", [])
if companies:
return companies[0]["id"]
# Create a new company
payload = {
"data": {
"type": "companies",
"attributes": {"name": company_name}
}
}
create_resp = requests.post(
f"{PRODUCTIVE_BASE}/companies",
json=payload,
headers=get_productive_headers(),
timeout=10
)
create_resp.raise_for_status()
return create_resp.json()["data"]["id"]
def create_project(name: str, company_id: str) -> str:
"""Create a project in Productive."""
payload = {
"data": {
"type": "projects",
"attributes": {
"name": name,
"project_color": "#4A90D9",
},
"relationships": {
"company": {
"data": {"type": "companies", "id": company_id}
}
}
}
}
resp = requests.post(
f"{PRODUCTIVE_BASE}/projects",
json=payload,
headers=get_productive_headers(),
timeout=10
)
resp.raise_for_status()
project_id = resp.json()["data"]["id"]
logging.info(f"Created project {project_id}: {name}")
return project_id
def create_budget(project_id: str, total: float, currency: str, billing_type: str) -> str:
"""
Create a budget for the project.
billing_type: 'fixed_price' | 'hourly'
"""
payload = {
"data": {
"type": "budgets",
"attributes": {
"name": "Project Budget",
"budget_total": total,
"currency_id": currency, # e.g. 'EUR', 'USD'
"billing_type": billing_type,
},
"relationships": {
"project": {
"data": {"type": "projects", "id": project_id}
}
}
}
}
resp = requests.post(
f"{PRODUCTIVE_BASE}/budgets",
json=payload,
headers=get_productive_headers(),
timeout=10
)
resp.raise_for_status()
budget_id = resp.json()["data"]["id"]
logging.info(f"Created budget {budget_id} for project {project_id}")
return budget_id
def assign_people_to_project(project_id: str, person_ids: list[str]):
"""Assign team members to the project."""
for person_id in person_ids:
payload = {
"data": {
"type": "project_assignments",
"relationships": {
"project": {"data": {"type": "projects", "id": project_id}},
"person": {"data": {"type": "people", "id": person_id}}
}
}
}
resp = requests.post(
f"{PRODUCTIVE_BASE}/project_assignments",
json=payload,
headers=get_productive_headers(),
timeout=10
)
if resp.status_code == 409: # Already assigned
logging.info(f"Person {person_id} already assigned to project {project_id}")
continue
resp.raise_for_status()
logging.info(f"Assigned person {person_id} to project {project_id}")
@app.route("/kommo/webhook", methods=["POST"])
def kommo_webhook():
"""Webhook from Kommo when a deal is won (status = won)."""
data = request.json
leads = data.get("leads", {}).get("status", [])
for lead_event in leads:
# Kommo status_id 142 for 'Won' may differ in your account
# Check via API: GET /api/v4/leads/pipelines
if lead_event.get("status_id") not in [142, 143]: # Your Won status IDs
continue
lead_id = lead_event["id"]
try:
lead = get_kommo_lead(lead_id)
# Extract custom fields
cf_values = {}
for cf in lead.get("custom_fields_values", []):
cf_values[cf["field_code"]] = cf["values"][0]["value"]
deal_name = lead.get("name", f"Deal {lead_id}")
deal_value = float(lead.get("price", 0))
company_name = cf_values.get("COMPANY", "Unknown Client")
service_type = cf_values.get("SERVICE_TYPE", "website")
billing_type = cf_values.get("BILLING_TYPE", "fixed_price")
team_emails = cf_values.get("ASSIGNED_TEAM", "").split(",")
# Map team members
person_ids = [
TEAM_MAP[email.strip()]
for email in team_emails
if email.strip() in TEAM_MAP
]
# Create objects in Productive
company_id = find_or_create_company(company_name)
project_id = create_project(deal_name, company_id)
create_budget(project_id, deal_value, "EUR", billing_type)
assign_people_to_project(project_id, person_ids)
logging.info(
f"Lead {lead_id} -> Project {project_id} created in Productive. "
f"Budget: {deal_value} EUR. Team: {person_ids}"
)
except requests.HTTPError as e:
logging.error(f"HTTP error for lead {lead_id}: {e.response.status_code} {e.response.text}")
except Exception as e:
logging.exception(f"Error processing lead {lead_id}: {e}")
return jsonify({"status": "ok"})
@app.route("/productive/webhook", methods=["POST"])
def productive_webhook():
"""
Webhook from Productive on task completion (task.completed)
or budget overrun (budget.exceeded).
"""
data = request.json
event_type = data.get("data", {}).get("type")
if event_type == "budget_notifications":
attrs = data["data"].get("attributes", {})
project_id = data["data"].get("relationships", {}).get("project", {}).get("data", {}).get("id")
logging.warning(f"Budget threshold exceeded for project {project_id}: {attrs}")
# Here: send a Slack notification or add a note to the deal in Kommo
return jsonify({"status": "ok"})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)
Step-by-Step Implementation
Step 1. Prepare Custom Fields in Kommo
Before the integration, make sure the deal card has the fields: SERVICE_TYPE (select), BILLING_TYPE (select: fixed_price/hourly), ASSIGNED_TEAM (text, comma-separated emails). Without this data the project will be created with default values. Funnel setup and custom fields are covered in the Kommo funnel setup article.
Step 2. Get the Organization ID in Productive
The Organization ID is visible in your Productive account URL: app.productive.io/organizations/{ORG_ID}/.... It is required in every request header as X-Organization-Id.
Step 3. Map Service Types
Get Service Type IDs in Productive via GET /api/v2/service_types. Create a SERVICE_MAP dictionary mapping your Kommo names to Productive IDs. This is a one-time configuration step.
Step 4. Configure the Kommo Webhook
Kommo -> Settings -> Webhooks. URL: https://your-server.com/kommo/webhook. Event: Lead status changed. Check the status_id for the Won status in your account via GET /api/v4/leads/pipelines - it is unique per account.
Step 5. Configure Productive Webhooks
Productive supports webhooks for task.completed and budget notification events. Configure in Settings -> Integrations -> Webhooks. Use for reverse notifications: when a project is finished you can update the deal status in Kommo or send a notification.
Real-World Case with Numbers
Digital agency in Berlin, 22 employees, 12-18 new projects per month. Before integration: the PM spent 40-60 minutes creating each project in Productive after a deal closed. That adds up to 18 hours per month just on manual data transfer. Budget errors: 2-3 projects per month started with the wrong amount due to manual entry.
After launching the integration: a project in Productive is created within 10-15 seconds of the status change in Kommo. The PM receives a Slack notification (via a separate webhook) that the project is ready. Budget errors: zero in the first three months. Savings: roughly 15 PM hours per month, now spent on actual planning instead of retyping data.
Additional effect: because budget_total is taken directly from the deal price in Kommo, reconciling “what we sold” vs “what we planned” became instant - the numbers are always identical.
Who This Is For
The Kommo + Productive integration is most valuable for digital agencies and service companies with 10-30 employees that handle 8-20 new projects per month. Prerequisite: Productive is already the primary project management and resource planning tool. If you are still evaluating options for tasks created from won deals, compare with Kommo + Asana and Kommo + Wrike: each tool has its own data model and a different level of budget detail.
Productive stands out because resource planning and financial tracking are its core features, not add-ons. If budget and team capacity matter more than kanban boards for your agency, Productive + Kommo via API solves this more cleanly than any no-code connector. More on the approach in custom Kommo integrations.
Frequently Asked Questions
How does Productive API authenticate requests?
Productive API v2 uses a Personal Access Token (PAT) in the Authorization: Bearer {token} header. Generate the token in account settings: Settings -> My Account -> API Access Tokens. In addition, all requests must include the X-Organization-Id header with your Productive organization ID (visible in the account URL). Documentation: developer.productive.io.
Can a project be created from a template in Productive via API?
Yes. The Productive API supports the POST /api/v2/projects/copy endpoint with a source_project_id parameter. This lets you copy a template project with standard tasks, milestones, and service structure. When copying you can pass copy_budgets: true and copy_assignees: false to preserve the financial structure while assigning a fresh team for the specific deal.
What should I do if person_id from Kommo does not match Productive?
Productive and Kommo are separate systems with independent user IDs. The mapping needs to be created manually once: fetch the list of people from Productive via GET /api/v2/people and build an email -> person_id dictionary. The mapping only needs updating when new employees are hired. Store the mapping in configuration or a database, not in code.
How do you handle budget overruns?
Productive sends a webhook notification when budget thresholds are reached (configured in Budget Settings). Your server receives the event and can: send a Slack notification to the PM, add a note to the deal card in Kommo, update a custom field with the current budget status. This lets the sales lead see the financial health of a project directly in Kommo without switching to Productive.
How do you pass employee rates to the Productive budget?
Productive supports Service Assignments with hourly rates. After creating the budget, create Services via POST /api/v2/services specifying service_type_id and hourly_rate. Then use POST /api/v2/service_assignments to attach specific people to services. Rates can be stored in Kommo custom fields or in the integration configuration - depending on whether they vary from project to project.
What’s Next
If your agency is losing 10+ hours per month transferring data from Kommo to Productive, this is a typical task for us. A properly configured integration runs without team involvement.
Describe your stack to the Exceltic.dev team: how your Kommo funnel is structured, which custom fields are filled at deal close, and how projects are organized in Productive. We will assess the scope and propose an architecture.