Discuss your task

Kommo + Jira: Auto-Create Tasks from Won Deals

When a deal moves to “Won”, Kommo sends a webhook to your backend, the backend calls POST /rest/api/3/issue on the Jira REST API v3, and returns the issue key (for example, DEV-47) back to the Kommo deal card. The whole thing takes about 3 seconds and requires no manual action from a sales manager.

This is not about Zapier and not about Kommo Marketplace. This is about a direct API-to-API connection with full control over every field - from the ADF issue description to Jira custom fields and idempotency on retries.

Companies running Kommo for sales and Jira for development often hit the same gap: sales closes a deal and waits. Developers have no idea a new client appeared until someone manually pings them. In integration projects at Exceltic.dev, this delay is consistently 1-2 business days, and higher during busy sales periods. This article covers the full flow: webhook -> issue creation -> writing the key back to CRM, including duplicate handling.

Why there is no native solution

Kommo Marketplace has Jira widgets, but they solve a different problem - displaying a Jira ticket inside the deal card. Creating an issue populated with data from multiple deal fields (amount, contact, custom fields) through those widgets is not possible. There is no two-way sync - the Jira task status does not flow back into Kommo.

Zapier supports creating a Jira issue when a Kommo status changes, but at 20+ deals per month the cost and reliability of Zapier become a problem: Zapier’s polling model delays event processing by 5-15 minutes and does not guarantee idempotency on retries. For teams where POST /rest/api/3/issue must fire exactly once, that is unacceptable.

Zapier is a rapid prototyping tool, not a production integration with delivery guarantees.

What a custom integration provides

Kommo webhook on transition to “Won”

Kommo sends a webhook when a deal status changes. The event type is leads.status; the payload contains the deal id, status_id (new), old_status_id, pipeline_id, and price. You register the leads.status event type in Kommo under Settings -> Integrations -> Webhooks.

Identifying a “won” deal requires matching pipeline_id + status_id. Kommo has no single global “won” status - each pipeline has its own stage IDs. To find the right ID: Settings -> Pipelines -> click the “Won” stage -> the status_id appears in the URL. Alternatively, call GET /api/v4/leads/pipelines - it returns all stages with id and type fields (type: 142 = “Won” in standard Kommo pipelines).

After receiving the webhook, the backend fetches full deal data with GET /api/v4/leads/{id}?with=contacts,companies - this gives you the contact and any custom fields that are not included in the webhook payload.

Jira REST API v3: creating an issue from deal data

Authentication: Basic Auth - Jira user email + API Token. Create an API Token at id.atlassian.com/manage-profile/security/api-tokens. Important: the token is tied to the user, not the project. Use a service account for integrations, not a personal one.

Basic Auth string: base64(email:api_token), passed in the Authorization: Basic <encoded> header.

Base URL: https://{your-site}.atlassian.net/rest/api/3/.

Minimum payload to create an issue:

import requests
from requests.auth import HTTPBasicAuth
import base64

JIRA_URL = 'https://yourcompany.atlassian.net'
JIRA_AUTH = HTTPBasicAuth('[email protected]', 'your_api_token')

def create_jira_issue_from_deal(lead: dict, contact: dict) -> str:
    """
    Creates a Jira issue from Kommo deal data.
    Returns the issue key, e.g. 'DEV-47'.
    """
    deal_amount = lead.get('price', 0)
    contact_name = contact.get('name', 'Unknown contact')
    contact_email = get_contact_email(contact)
    deal_name = lead.get('name', f'Deal #{lead["id"]}')

    # Description in Atlassian Document Format (ADF) - required for Jira Cloud
    description_text = (
        f'Client: {contact_name}\n'
        f'Email: {contact_email}\n'
        f'Deal amount: ${deal_amount}\n'
        f'Kommo Deal ID: {lead["id"]}\n'
        f'Owner: {lead.get("responsible_user_id")}'
    )

    payload = {
        'fields': {
            'project': {'key': 'DEV'},         # Jira project key
            'issuetype': {'name': 'Task'},      # Task, Story, Bug, etc.
            'summary': f'[CRM] {deal_name}',
            'description': {
                'type': 'doc',
                'version': 1,
                'content': [{
                    'type': 'paragraph',
                    'content': [{
                        'type': 'text',
                        'text': description_text
                    }]
                }]
            },
            'priority': {'name': 'Medium'},
            # custom field to store Kommo Deal ID
            # look up the custom field ID via GET /rest/api/3/field
            'customfield_10200': str(lead['id'])
        }
    }

    resp = requests.post(
        f'{JIRA_URL}/rest/api/3/issue',
        auth=JIRA_AUTH,
        json=payload
    )
    resp.raise_for_status()
    return resp.json()['key']  # 'DEV-47'

To find the custom field ID for kommo_deal_id - call GET /rest/api/3/field once and locate the right field in the response. The ID will look like customfield_XXXXX. This is needed for reverse lookup: when an issue is closed in Jira, you need to find the corresponding deal in Kommo.

Writing the Jira issue key back to Kommo

After creating the issue, the backend writes the key back to Kommo via PATCH /api/v4/leads/{id} into a custom field jira_ticket_key. It then adds a Note via POST /api/v4/leads/{id}/notes. The manager sees the key directly in the deal card without logging into Jira.

def write_jira_key_to_kommo(
    deal_id: int,
    jira_key: str,
    jira_base_url: str,
    kommo_access_token: str
) -> None:
    headers = {'Authorization': f'Bearer {kommo_access_token}'}
    jira_url = f'{jira_base_url}/browse/{jira_key}'

    # Write key to Kommo custom field
    requests.patch(
        f'https://yourcommo.kommo.com/api/v4/leads/{deal_id}',
        headers=headers,
        json={'custom_fields_values': [{
            'field_id': JIRA_KEY_FIELD_ID,
            'values': [{'value': jira_key}]
        }]}
    )

    # Add a note to the deal card
    requests.post(
        f'https://yourcommo.kommo.com/api/v4/leads/{deal_id}/notes',
        headers=headers,
        json=[{'note_type': 4, 'params': {
            'text': f'Jira issue created: {jira_key} - {jira_url}'
        }}]
    )

Idempotency: protection against duplicates

Kommo can send the same webhook twice - on an unstable connection or when the status changes more than once. Without protection, each webhook creates a new Jira issue.

The solution is to store a kommo_deal_id -> jira_issue_key mapping in Redis or PostgreSQL. Before calling POST /rest/api/3/issue, check: if a key already exists for this deal_id - skip creation and return the existing key. This is a standard idempotency pattern for webhook handlers.

def handle_kommo_webhook(payload: dict) -> None:
    lead_data = payload.get('leads', {}).get('status', [])
    if not lead_data:
        return

    lead = lead_data[0]
    deal_id = int(lead['id'])
    pipeline_id = int(lead.get('pipeline_id', 0))
    status_id = int(lead.get('status_id', 0))

    # Confirm this is the Won stage of the target pipeline
    if not is_won_status(pipeline_id, status_id):
        return

    # Idempotency: check for an existing key
    existing_key = get_jira_key_for_deal(deal_id)  # from Redis/DB
    if existing_key:
        return  # duplicate, skip

    # Fetch full deal data
    full_lead, contact = fetch_lead_with_contact(deal_id)

    # Create issue in Jira
    jira_key = create_jira_issue_from_deal(full_lead, contact)

    # Save mapping
    save_jira_key_for_deal(deal_id, jira_key)

    # Write result back to Kommo
    write_jira_key_to_kommo(deal_id, jira_key, JIRA_URL, KOMMO_TOKEN)

Step-by-step flow

  1. Deal moves to the “Won” stage in Kommo
  2. Kommo sends POST to your endpoint with the leads.status event
  3. Backend checks pipeline_id + status_id - is this Won?
  4. Idempotency check: has an issue already been created for this deal_id?
  5. If not - GET /api/v4/leads/{id}?with=contacts to fetch full deal data
  6. POST /rest/api/3/issue in Jira with ADF description and custom fields
  7. Jira returns key (for example, DEV-47)
  8. Save mapping deal_id -> DEV-47 to storage
  9. PATCH /api/v4/leads/{id} - write the key to a Kommo custom field
  10. POST /api/v4/leads/{id}/notes - Note to the manager: “Issue DEV-47 created”
  11. Webhook from Jira on issue close (Done) -> Note in Kommo: “Issue DEV-47 closed”

Real case with numbers

B2B SaaS company (Europe, 25-35 new clients per month): sales team in Kommo, dev and CS teams in Jira. The average handoff delay after Won was 1.5 days - the manager closed a deal, sent an email or Slack message, and the developer then created a ticket manually with whatever information they could gather.

After implementing the custom integration:

  • Time from Won to issue appearing in Jira: under 10 seconds (including webhook delivery)
  • 0 issues with empty descriptions - contact, email, amount, and plan are populated automatically from deal fields
  • Manager sees the issue status (In Progress / Done) directly in the Kommo card via reverse webhook
  • Savings: roughly 20 minutes per deal (manual creation + information lookup + follow-up questions)

At 30 deals per month that is about 10 hours per month - just on context handoff between teams. Plus eliminating errors caused by incomplete information in issues.

For comparison, the Exceltic.dev team also built a Kommo integration with ClickUp for another client where the logic is identical, but the ClickUp REST API is simpler - there is no ADF description format.

Who needs this

This integration makes sense if:

  • You run sales in Kommo and track dev / CS / onboarding work in Jira
  • After Won, the client requires technical onboarding, development, or configuration - and that gets tracked as an issue
  • 15+ client handoffs per month - manual ticket creation takes a noticeable amount of time
  • You need two-way visibility: the manager must see the Jira issue status directly in the CRM

The Zapier option does not work if you have strict idempotency requirements or enough volume to justify custom development.

More on how to build integrations and custom development for Kommo is covered in a dedicated section.

Term: Atlassian Document Format (ADF) - a JSON structure for rich text in Jira Cloud. Unlike Jira Server/Data Center where description accepts plain text, Jira Cloud requires an ADF document with an explicit type: doc, version: 1, and a content tree. Without the correct format, POST /rest/api/3/issue returns 400 Bad Request.

Frequently asked questions

Why is a custom integration better than Zapier for Kommo -> Jira?

Zapier uses a polling model with a 5-15 minute interval, does not guarantee idempotency on failure, and cannot populate Jira custom fields from multiple deal fields simultaneously. A custom webhook handler reacts to events in real time, supports idempotency via a data store, and gives full control over the issue structure - including ADF description, labels, priority, and custom fields.

How does Kommo pass data about a won deal?

Kommo sends a POST request with a leads.status event in the payload. Webhook payload fields: id (deal ID), status_id (new status), old_status_id, pipeline_id, price. Contacts and custom fields are not included in the webhook - they must be fetched separately via GET /api/v4/leads/{id}?with=contacts. The “Won” stage is identified by the pipeline_id + status_id combination, or via type: 142 in the GET /api/v4/leads/pipelines response.

Is OAuth required for the Jira API or is Basic Auth enough?

For a server-side integration with no user interaction - Basic Auth with email and an API Token is sufficient. Create the API Token at id.atlassian.com/manage-profile/security/api-tokens. OAuth 2.0 is only needed for public apps where each user authorizes access on their own behalf. As of 2026, Atlassian was planning to introduce expiry for API tokens - check the current policy in the Atlassian documentation.

What is ADF and why is it required in Jira Cloud?

ADF (Atlassian Document Format) is the required format for the description field in the Jira Cloud REST API v3. Minimum structure: {"type": "doc", "version": 1, "content": [{"type": "paragraph", "content": [{"type": "text", "text": "text"}]}]}. Passing a plain string returns a 400. In Jira Server/Data Center (non-cloud), description accepts plain text or wiki-markup - that is a different API.

How to prevent duplicates from repeated Kommo webhooks?

Store the kommo_deal_id -> jira_issue_key mapping in Redis or PostgreSQL. At the start of each webhook handler, check: if the record already exists - skip issue creation. This is the standard idempotency pattern. Additionally: Kommo may resend a webhook on a network error (no 200 response within 2 seconds) - so your endpoint must return 200 even when skipping a duplicate.

What if we need an integration with Kommo and other task trackers?

The logic is identical for most task trackers: Kommo webhook on Won -> call the tracker API -> write the task ID back to CRM. The only differences are API details: ClickUp uses REST with a simple JSON body, Linear uses GraphQL with mutations. Jira stands out for its mandatory ADF description format and the need to store custom field IDs for traceability.

Summary

  • Kommo webhook leads.status + matching pipeline_id + status_id - reliable Won identification
  • Jira REST API v3: Basic Auth (email + API Token), POST /rest/api/3/issue with mandatory ADF description
  • Idempotency via deal_id -> issue_key storage - prevents duplicates on retries
  • Writing issue_key back to Kommo via PATCH /leads/{id} and a Note - manager sees issue status without Jira
  • Typical development timeline: 1-2 weeks including field mapping configuration

If you are running Kommo and Jira and client handoffs still happen manually - describe your setup: which Jira projects, which deal fields need to go into the issue. The Exceltic.dev team will review the architecture and estimate the scope.

More articles

All →