Kommo + Signeasy: электронная подпись документов из карточки сделки

Signeasy - платформа электронной подписи с поддержкой eIDAS (EU), ESIGN Act (US), и IT Act (Индия). Позиционируется как более простой и доступный альтернатив DocuSign для малого и среднего бизнеса. Поддерживает шаблоны, поля для заполнения, последовательную и параллельную подпись. Кастомная интеграция с Kommo решает главную проблему нативных eSign-коннекторов: конверт создаётся и подписывается, но статус не попадает обратно в карточку сделки.

Signeasy REST API работает с Bearer-токеном. Ключевые операции: создать запрос на подпись из шаблона, отслеживать статус, получить уведомление о подписании через webhook.

Signeasy Template - предварительно заготовленный документ с полями для заполнения (имя, дата, сумма). При создании запроса на подпись поля заполняются данными из CRM-сделки.

Что не работает без интеграции

Стандартный процесс: менеджер скачивает шаблон -> вручную вносит данные клиента -> загружает в Signeasy -> отправляет на подпись -> следит за статусом вне Kommo.

Два разрыва: данные из сделки не попадают в документ автоматически, статус подписания не возвращается в Kommo.

Архитектура

Kommo: сделка -> этап "Отправить контракт"
  -> Kommo webhook -> Ваш сервер

Ваш сервер
  -> Получить данные сделки (имя, email, сумма)
  -> Signeasy API: POST /v1/signature_requests
     {template_id, signer.email, signer.name, fields: {amount, company}}
  -> Kommo: записать signeasy_request_id в custom field

Клиент подписывает -> Signeasy
  -> Signeasy webhook: signature_request.signed
  -> Ваш сервер
  -> Kommo: перевести сделку -> следующий этап
  -> Kommo: добавить ссылку на подписанный PDF

Реализация: отправка на подпись

import requests, os, hmac, hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)

SE_TOKEN        = os.environ["SIGNEASY_API_TOKEN"]
SE_TEMPLATE_ID  = os.environ["SIGNEASY_TEMPLATE_ID"]
SE_WEBHOOK_SEC  = os.environ["SIGNEASY_WEBHOOK_SECRET"]

KOMMO_SUBDOMAIN = os.environ["KOMMO_SUBDOMAIN"]
KOMMO_TOKEN     = os.environ["KOMMO_ACCESS_TOKEN"]
SIGN_STAGE_ID   = int(os.environ["KOMMO_SIGN_STAGE_ID"])
SIGNED_STAGE_ID = int(os.environ["KOMMO_SIGNED_STAGE_ID"])
CF_REQUEST_ID   = int(os.environ["KOMMO_CF_SIGNEASY_REQUEST_ID"])

SE_BASE   = "https://api.signeasy.com"
SE_HDR    = {"Authorization": f"Bearer {SE_TOKEN}", "Content-Type": "application/json"}
KOMMO_BASE = f"https://{KOMMO_SUBDOMAIN}.kommo.com/api/v4"
KOMMO_HDR  = {"Authorization": f"Bearer {KOMMO_TOKEN}", "Content-Type": "application/json"}

def get_lead_with_contact(lead_id: int) -> tuple[dict, dict]:
    r = requests.get(
        f"{KOMMO_BASE}/leads/{lead_id}",
        headers=KOMMO_HDR,
        params={"with": "contacts,custom_fields_values"},
    )
    lead = r.json()
    contacts = lead.get("_embedded", {}).get("contacts", [])
    contact = {}
    if contacts:
        rc = requests.get(
            f"{KOMMO_BASE}/contacts/{contacts[0]['id']}",
            headers=KOMMO_HDR,
            params={"with": "custom_fields_values"},
        )
        contact = rc.json()
    return lead, contact

def extract_email(contact: dict) -> str:
    for cf in contact.get("custom_fields_values", []) or []:
        if cf.get("field_code") == "EMAIL":
            vals = cf.get("values", [])
            if vals:
                return vals[0].get("value", "")
    return ""

def create_signature_request(signer_name: str, signer_email: str, fields: dict) -> str:
    payload = {
        "template_id":  SE_TEMPLATE_ID,
        "message":      "Пожалуйста, подпишите договор.",
        "signers": [{
            "name":  signer_name,
            "email": signer_email,
            "role":  "Signer",
        }],
        "prefill_fields": [
            {"api_key": k, "value": str(v)}
            for k, v in fields.items()
        ],
    }
    r = requests.post(f"{SE_BASE}/v1/signature_requests", headers=SE_HDR, json=payload)
    r.raise_for_status()
    return r.json()["id"]

def save_request_id(lead_id: int, request_id: str):
    requests.patch(
        f"{KOMMO_BASE}/leads/{lead_id}",
        headers=KOMMO_HDR,
        json={"custom_fields_values": [{
            "field_id": CF_REQUEST_ID,
            "values":   [{"value": request_id}],
        }]},
    )

@app.route("/webhooks/kommo", methods=["POST"])
def kommo_webhook():
    data = request.json or {}
    for lead_data in data.get("leads", {}).get("status", []):
        lead_id    = lead_data.get("id")
        new_status = lead_data.get("status_id")
        if new_status != SIGN_STAGE_ID:
            continue

        lead, contact = get_lead_with_contact(lead_id)
        signer_email  = extract_email(contact)
        signer_name   = contact.get("name", "")

        if not signer_email:
            continue

        fields = {
            "company_name":    lead.get("name", ""),
            "contract_amount": str(lead.get("price", 0)),
            "kommo_lead_id":   str(lead_id),
        }

        req_id = create_signature_request(signer_name, signer_email, fields)
        save_request_id(lead_id, req_id)

    return jsonify({"status": "ok"}), 200

Реализация: webhook при подписании

def verify_signeasy_signature(body: bytes, sig_header: str) -> bool:
    # Signeasy HMAC-SHA256 hex digest
    computed = hmac.new(SE_WEBHOOK_SEC.encode(), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(computed, sig_header)

@app.route("/webhooks/signeasy", methods=["POST"])
def signeasy_webhook():
    sig = request.headers.get("X-Signeasy-Signature", "")
    if not verify_signeasy_signature(request.data, sig):
        return jsonify({"error": "invalid signature"}), 401

    event     = request.json or {}
    event_type = event.get("event_type", "")

    if event_type not in ("signature_request.signed", "signature_request.declined"):
        return jsonify({"status": "ignored"}), 200

    # Найти kommo_lead_id в prefill_fields
    doc       = event.get("document", {})
    prefills  = doc.get("prefill_fields", [])
    lead_id   = None
    for f in prefills:
        if f.get("api_key") == "kommo_lead_id":
            lead_id = f.get("value")
            break

    if not lead_id:
        return jsonify({"status": "no_lead_id"}), 200

    if event_type == "signature_request.signed":
        signed_url = doc.get("signed_document_url", "")
        # Перевести сделку в следующий этап
        requests.patch(
            f"{KOMMO_BASE}/leads/{lead_id}",
            headers=KOMMO_HDR,
            json={"status_id": SIGNED_STAGE_ID},
        )
        # Добавить ссылку на подписанный PDF
        requests.post(
            f"{KOMMO_BASE}/notes",
            headers=KOMMO_HDR,
            json=[{
                "entity_id":   int(lead_id),
                "entity_type": "leads",
                "note_type":   "common",
                "params":      {"text": f"Signeasy: договор подписан. PDF: {signed_url}"},
            }],
        )
    elif event_type == "signature_request.declined":
        decliner = event.get("signer", {}).get("name", "клиент")
        requests.post(
            f"{KOMMO_BASE}/notes",
            headers=KOMMO_HDR,
            json=[{
                "entity_id":   int(lead_id),
                "entity_type": "leads",
                "note_type":   "common",
                "params":      {"text": f"Signeasy: договор отклонён подписантом {decliner}. Уточните причину."},
            }],
        )

    return jsonify({"status": "ok"}), 200

Настройка Signeasy

  1. Signeasy -> Settings -> API Access -> Generate API Token
  2. Templates -> создать шаблон контракта с полями company_name, contract_amount, kommo_lead_id
    • Поле kommo_lead_id скрытое (Hidden field) - заполняется API, не показывается подписанту
  3. Webhooks -> Add webhook endpoint
    • URL: https://your-server.com/webhooks/signeasy
    • Events: signature_request.signed, signature_request.declined, signature_request.expired
  4. Скопировать Webhook Secret для HMAC-верификации

Поддержка eIDAS (EU)

Signeasy поддерживает Simple Electronic Signature (SES) - соответствует требованиям eIDAS для большинства B2B-контрактов в ЕС. Для контрактов, требующих Advanced Electronic Signature (AES) или Qualified (QES) - нужны провайдеры с EU Trust List (Yousign, Scrive).

Реальный кейс

SaaS-компания, 8 AE, 30 контрактов в месяц. До интеграции: менеджеры тратили 20 минут на каждый контракт (данные из Kommo -> Word -> PDF -> Signeasy). После: запрос создаётся автоматически при переводе сделки в “Отправить контракт”. Экономия: 10 часов в месяц на команду.

Для кого актуально

B2B SaaS и сервисные компании с циклом продаж 15-60 дней, контрактами на $1000+ и командой продаж 3-20 человек. Особенно если документооборот - узкое место (задержки с подписанием тормозят revenue recognition).

Похожий подход описан для Kommo + DocuSign и Kommo + Dropbox Sign.

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

Чем Signeasy отличается от DocuSign и Adobe Sign?

Signeasy дешевле ($8-24/user/mo vs $15-45 у DocuSign) и проще в настройке API. Не имеет нативной интеграции с HubSpot или Salesforce из коробки - только через API или Zapier. Для небольших команд (до 20 человек) и простых контрактов - полноценная альтернатива.

Как добавить несколько подписантов (sequential signing)?

В signers передать массив: [{name, email, role, signing_order: 1}, {name, email, role, signing_order: 2}]. Второй подписант получит запрос только после того как первый подпишет. Webhook signature_request.signed приходит когда все подписанты завершили.

Можно ли скачать подписанный PDF через API?

Да: GET /v1/signature_requests/{id}/download - вернёт signed PDF. Можно загрузить его в Kommo как attachment через Kommo Files API. Это полезно для архивирования - подписанный документ хранится прямо в карточке сделки.

Итог

Kommo + Signeasy - автоматизация документооборота в воронке продаж:

  • Kommo webhook leads.status.changed -> POST /v1/signature_requests с prefill_fields
  • Скрытое поле kommo_lead_id для обратной корреляции
  • HMAC-SHA256 верификация webhook (X-Signeasy-Signature)
  • signature_request.signed -> перевести Deal + добавить ссылку на PDF
  • signature_request.declined -> создать задачу менеджеру

Если нужна интеграция Kommo с Signeasy или другим eSign-инструментом - обратитесь в Exceltic.dev.

Ещё статьи

Все →