Kommo + Concord: автоматическая отправка контрактов

Kommo + Concord: автоматическая отправка контрактов

При переходе сделки в «Выиграно» Kommo автоматически создаёт документ из шаблона в Concord CLM, подставляет данные клиента и отправляет на подписание. Ссылка на контракт и его статус возвращаются обратно в карточку CRM.

При закрытии сделки менеджер вручную копирует имя компании, сумму, реквизиты и условия в шаблон контракта в Concord, затем отправляет его контрагенту и ждёт. При 30-50 сделках в месяц это около 3-4 часов ручного заполнения и постоянный источник ошибок: неверная сумма, старая версия шаблона, перепутанные реквизиты. Один такой контракт с ошибкой может задержать закрытие сделки на неделю. Concord - CLM-платформа (Contract Lifecycle Management) с поддержкой шаблонов, переменных и workflow согласования. Её API позволяет создавать документы программно, передавая значения переменных из внешних систем. В статье - архитектура двунаправленной интеграции, Python-код и кейс B2B SaaS-компании.

Почему ручной process документооборота не масштабируется

Kommo не имеет нативной интеграции с Concord. Существующие решения через Zapier упираются в одно ограничение: Zapier умеет триггерить создание документа в Concord, но не умеет корректно маппировать сложные переменные шаблона (вложенные объекты, условные блоки) и не обрабатывает обратный webhook при подписании.

В итоге компании получают полуавтоматику: документ создаётся автоматически, но менеджер всё равно заходит в Concord, проверяет данные, исправляет ошибки маппинга и только потом отправляет. Ценность автоматизации падает до нуля.

Кастомная интеграция через API закрывает весь цикл: создание документа, отправка, отслеживание статуса, запись результата в CRM.

Архитектура интеграции

Двунаправленный поток данных:

Kommo -> Concord (при выигрыше сделки):

  • Webhook от Kommo на ваш сервис
  • Сервис запрашивает полные данные сделки и контакта через Kommo API
  • Создаёт документ из шаблона в Concord через POST /1/documents
  • Записывает URL контракта обратно в кастомное поле Kommo

Concord -> Kommo (при изменении статуса контракта):

  • Webhook от Concord при document.signed, document.countersigned, document.expired
  • Сервис обновляет кастомное поле «Статус контракта» в Kommo
  • Добавляет заметку с датой и участниками подписания

Аутентификация Concord API: Bearer token (Authorization: Bearer <api_key>) из раздела Settings -> API в вашем Concord аккаунте.

import httpx
from fastapi import FastAPI, Request, HTTPException
import logging

app = FastAPI()
logger = logging.getLogger(__name__)

CONCORD_API_KEY = "your_concord_api_key"
CONCORD_TEMPLATE_ID = "template_uuid_here"  # ID шаблона в Concord
KOMMO_SUBDOMAIN = "your_company"
KOMMO_TOKEN = "your_kommo_token"

# ID кастомных полей в Kommo (узнать через GET /api/v4/leads/custom_fields)
FIELD_CONTRACT_URL = 123456
FIELD_CONTRACT_STATUS = 123457


async def get_kommo_deal(deal_id: int) -> dict:
    """Получает данные сделки и связанного контакта."""
    async with httpx.AsyncClient() as client:
        resp = await client.get(
            f"https://{KOMMO_SUBDOMAIN}.kommo.com/api/v4/leads/{deal_id}",
            headers={"Authorization": f"Bearer {KOMMO_TOKEN}"},
            params={"with": "contacts"},
            timeout=10.0
        )
        resp.raise_for_status()
        return resp.json()


async def get_kommo_contact(contact_id: int) -> dict:
    """Получает данные контакта (email, телефон, компания)."""
    async with httpx.AsyncClient() as client:
        resp = await client.get(
            f"https://{KOMMO_SUBDOMAIN}.kommo.com/api/v4/contacts/{contact_id}",
            headers={"Authorization": f"Bearer {KOMMO_TOKEN}"},
            timeout=10.0
        )
        resp.raise_for_status()
        return resp.json()


async def create_concord_document(deal_data: dict, contact_data: dict) -> dict:
    """
    Создаёт документ из шаблона в Concord.
    Маппинг переменных шаблона из полей Kommo.
    """
    # Извлекаем нужные поля из данных Kommo
    custom_fields = {cf["field_id"]: cf["values"][0]["value"]
                     for cf in deal_data.get("custom_fields_values", [])}

    # Переменные для шаблона Concord
    template_variables = {
        "client_name": contact_data.get("name", ""),
        "company_name": contact_data.get("company", {}).get("name", ""),
        "client_email": next(
            (v["value"] for v in contact_data.get("custom_fields_values", [])
             if v.get("field_type") == "EMAIL"), ""
        ),
        "deal_amount": str(deal_data.get("price", 0)),
        "deal_name": deal_data.get("name", ""),
        "deal_id": str(deal_data.get("id", "")),
        # Дополнительные поля из custom fields Kommo:
        "contract_start_date": custom_fields.get(111111, ""),
        "subscription_term": custom_fields.get(111112, "12 месяцев"),
    }

    async with httpx.AsyncClient() as client:
        resp = await client.post(
            "https://api.concord.app/1/documents",
            headers={
                "Authorization": f"Bearer {CONCORD_API_KEY}",
                "Content-Type": "application/json"
            },
            json={
                "template_id": CONCORD_TEMPLATE_ID,
                "title": f"Contract - {deal_data.get('name', '')} #{deal_data.get('id')}",
                "variables": template_variables,
                "send_for_signature": True  # автоматически отправить на подписание
            },
            timeout=15.0
        )
        resp.raise_for_status()
        return resp.json()


async def update_kommo_deal_fields(deal_id: int, contract_url: str, status: str):
    """Обновляет кастомные поля сделки Kommo."""
    async with httpx.AsyncClient() as client:
        await client.patch(
            f"https://{KOMMO_SUBDOMAIN}.kommo.com/api/v4/leads/{deal_id}",
            headers={"Authorization": f"Bearer {KOMMO_TOKEN}"},
            json={
                "custom_fields_values": [
                    {"field_id": FIELD_CONTRACT_URL, "values": [{"value": contract_url}]},
                    {"field_id": FIELD_CONTRACT_STATUS, "values": [{"value": status}]}
                ]
            },
            timeout=10.0
        )


@app.post("/kommo/webhook")
async def handle_kommo_webhook(request: Request):
    """Обрабатывает переход сделки в Won, создаёт контракт в Concord."""
    data = await request.json()
    for lead in data.get("leads", {}).get("status", []):
        if lead.get("status_id") == 142:  # Won
            deal_id = lead["id"]
            deal_data = await get_kommo_deal(deal_id)

            # Берём первый связанный контакт
            contact_id = deal_data["_embedded"]["contacts"][0]["id"]
            contact_data = await get_kommo_contact(contact_id)

            doc = await create_concord_document(deal_data, contact_data)
            contract_url = doc.get("document_url", "")

            await update_kommo_deal_fields(deal_id, contract_url, "Sent")
            logger.info(f"Contract created for deal {deal_id}: {contract_url}")

    return {"status": "ok"}


@app.post("/concord/webhook")
async def handle_concord_webhook(request: Request):
    """Обрабатывает события подписания контракта из Concord."""
    event = await request.json()
    event_type = event.get("event")
    document = event.get("document", {})

    # Извлекаем deal_id из переменных документа
    variables = document.get("variables", {})
    deal_id_str = variables.get("deal_id")
    if not deal_id_str:
        return {"status": "skipped"}

    deal_id = int(deal_id_str)

    status_map = {
        "document.signed": "Signed",
        "document.countersigned": "Countersigned",
        "document.expired": "Expired",
    }
    new_status = status_map.get(event_type, "Unknown")
    contract_url = document.get("document_url", "")

    await update_kommo_deal_fields(deal_id, contract_url, new_status)
    return {"status": "ok"}

Пошаговая реализация

Шаг 1. Подготовка шаблона в Concord

В Concord создайте или адаптируйте шаблон контракта. Переменные задаются двойными фигурными скобками: {{client_name}}, {{deal_amount}}, {{contract_start_date}}. Убедитесь что шаблон работает при ручном тесте. Сохраните UUID шаблона из URL или через API.

Шаг 2. Маппинг полей Kommo

Определите, какие поля сделки и контакта нужны для заполнения шаблона. Получите список кастомных полей через GET /api/v4/leads/custom_fields. Стандартные поля сделки (name, price, status) доступны напрямую.

Шаг 3. Webhook из Kommo

Настройте webhook в Kommo на событие смены этапа воронки. Используйте конкретный pipeline_id и status_id этапа «Выиграно», чтобы не обрабатывать лишние события.

Шаг 4. Webhook в Concord

По документации Concord настройте webhook на события документа. Укажите URL вашего сервиса и подпишитесь на document.signed, document.countersigned, document.expired.

Шаг 5. Хранение deal_id в переменных документа

Ключевой паттерн: передавайте deal_id как переменную шаблона (даже если она не отображается в тексте контракта). Это позволяет в обратном webhook точно определить, какую сделку обновлять.

Шаг 6. Обработка ошибок

Concord API может вернуть 422 если переменная шаблона не заполнена или шаблон не найден. Логируйте детали ошибки и отправляйте уведомление в Slack/email - это позволит быстро исправить маппинг без потери сделки.

Реальный кейс: B2B SaaS, 35 сделок в месяц

Клиент - B2B SaaS-компания с командой продаж 8 человек в Европе. До интеграции: AE закрывает звонок, переключается в Concord, вручную создаёт документ из шаблона, заполняет 7-8 переменных, проверяет данные, отправляет. Среднее время - 15-20 минут на контракт. Ошибки (неверная сумма, старая версия шаблона) - 2-3 в месяц.

После интеграции: при смене этапа в Kommo контракт создаётся и уходит клиенту автоматически за 5-10 секунд. AE видит ссылку в карточке, статус «Sent» меняется на «Signed» автоматически.

Результат:

  • Экономия: ~9 часов в месяц (35 сделок x 15 мин)
  • Ошибки данных в контрактах: снизились до нуля
  • Скорость подписания выросла: клиент получает контракт пока настроение позитивное, а не через час
  • Руководитель видит в Kommo список сделок с неподписанными контрактами - раньше это требовало ручной сверки

Компания дополнительно настроила триггер: если контракт не подписан за 48 часов, Kommo автоматически создаёт задачу для менеджера с пометкой «follow up».

Для кого подходит эта интеграция

  • Компании с 20+ сделками в месяц, где каждая требует контракта
  • B2B SaaS, агентства, консалтинг - где шаблон контракта стандартизирован
  • Команды, где AE закрывает сделку и сам же занимается документооборотом
  • Ситуации с несколькими подписантами: Concord поддерживает последовательное и параллельное подписание

Если вы уже используете DocuSign или Dropbox Sign, Concord предлагает схожую функциональность с фокусом на управление жизненным циклом контрактов и встроенные инструменты согласования.

Часто задаваемые вопросы

Concord поддерживает юридически значимую подпись в EU (eIDAS)?

Concord предоставляет электронную подпись, соответствующую требованиям eIDAS в категории Simple Electronic Signature (SES). Для большинства B2B-контрактов этого достаточно. Если вам нужна Advanced или Qualified Electronic Signature, рассмотрите специализированные EU-платформы (Yousign, Docuseal с EU Cloud). Проверяйте текущий статус соответствия в документации Concord перед финальным выбором.

Как передать в контракт данные из нескольких контактов (например, два подписанта)?

Concord API позволяет указать список подписантов с email-адресами при создании документа через поле signers. В Kommo создайте кастомные поля для второго контакта-подписанта и передавайте их в теле запроса к API. Порядок подписания (последовательный или параллельный) настраивается через параметр signing_order.

Что если сделка была переведена в Won по ошибке и нужно отозвать контракт?

Concord позволяет отозвать документ через API (DELETE /1/documents/{id} или через dashboard). Рекомендуем добавить задержку 2-5 минут между триггером в Kommo и созданием контракта - это даст менеджеру время заметить ошибку. Также можно добавить кастомное поле-флаг «Не создавать контракт автоматически» для исключительных случаев.

Можно ли использовать разные шаблоны для разных типов сделок?

Да. В Kommo создайте кастомное поле «Тип контракта» или используйте название воронки / этапа для выбора нужного template_id в Concord. Логика выбора шаблона реализуется на стороне вашего webhook-сервиса простым словарём {deal_type: template_uuid}.

Как протестировать интеграцию без отправки реальных контрактов клиентам?

Concord поддерживает тестовый режим через sandbox-аккаунт. В Kommo создайте тестовую воронку или тестовый этап для QA. Параметр send_for_signature: false позволяет создать документ без немедленной отправки - полезно для проверки маппинга данных.


Если ваша команда тратит 10+ часов в месяц на заполнение контрактов вручную - опишите задачу команде Exceltic.dev. Это типовая задача для нас: разберём ваш стек, маппинг шаблонов и предложим решение.

Ещё статьи

Все →