Kommo + Documenso: open-source электронная подпись из воронки без вендор-локина

Documenso - open-source альтернатива DocuSign: электронная подпись, шаблоны, поля форм, audit trail. Self-hosted или облако (app.documenso.com). Ключевое преимущество - отсутствие подписки на $15-45/user/mo, которая характерна для DocuSign и Adobe Sign. Для компаний с собственной инфраструктурой Documenso self-hosted + интеграция с Kommo даёт полный цикл: создать документ на подпись из карточки сделки, получить статус в Kommo, архивировать подписанный PDF.

Documenso REST API использует Bearer token (Personal Access Token из настроек аккаунта). Основные операции: создать документ из template, отправить на подпись, получить статус и скачать подписанный PDF. Webhooks отправляются при изменении статуса документа.

Documenso Template - предварительно заготовленный документ с настроенными полями для заполнения. Аналог DocuSign Template. При создании запроса на подпись поля заполняются данными из CRM.

Self-hosted vs облако

Documenso Cloud (app.documenso.com): платный, но значительно дешевле DocuSign. API и webhooks доступны на всех планах.

Self-hosted: полностью бесплатно, Docker Compose развертывание за 30 минут. Данные хранятся на вашей инфраструктуре. Поддерживает GDPR/HIPAA при правильной настройке.

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

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

Ваш сервер
  -> Получить данные сделки (имя, email, сумма)
  -> Documenso API: создать документ из template
  -> Documenso API: добавить signer + prefill поля
  -> Documenso API: отправить на подпись
  -> Kommo: записать document_id в custom field

Клиент подписывает
  -> Documenso webhook: document.completed
  -> Ваш сервер -> Kommo: Closed Won + ссылка на PDF

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

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

app = Flask(__name__)

DS_TOKEN      = os.environ["DOCUMENSO_API_TOKEN"]
DS_BASE       = os.environ.get("DOCUMENSO_BASE", "https://app.documenso.com")
DS_TEMPLATE   = os.environ["DOCUMENSO_TEMPLATE_ID"]
DS_WEBHOOK_SC = os.environ["DOCUMENSO_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"])
KOMMO_CF_DOC_ID   = int(os.environ["KOMMO_CF_DOCUMENSO_ID"])

DOCUMENSO_BASE = f"{DS_BASE}/api/v1"
DS_HDR         = {"Authorization": f"Bearer {DS_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_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 get_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_documenso_doc(signer_name: str, signer_email: str, fields: dict) -> str:
    # Шаг 1: создать документ из шаблона
    r = requests.post(
        f"{DOCUMENSO_BASE}/templates/{DS_TEMPLATE}/create-document",
        headers=DS_HDR,
        json={
            "title":    f"Договор - {signer_name}",
            "signers": [{
                "name":  signer_name,
                "email": signer_email,
                "role":  "SIGNER",
            }],
            "formValues": fields,  # prefill полей шаблона
        },
    )
    r.raise_for_status()
    doc_id = r.json().get("documentId", "")

    # Шаг 2: отправить на подпись
    requests.post(f"{DOCUMENSO_BASE}/documents/{doc_id}/send", headers=DS_HDR)
    return str(doc_id)

def save_doc_id(lead_id: int, doc_id: str):
    requests.patch(
        f"{KOMMO_BASE}/leads/{lead_id}",
        headers=KOMMO_HDR,
        json={"custom_fields_values": [{
            "field_id": KOMMO_CF_DOC_ID,
            "values":   [{"value": doc_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_contact(lead_id)
        email         = get_email(contact)
        if not email:
            continue

        signer_name = contact.get("name", "")
        fields = {
            "client_name":  signer_name,
            "deal_amount":  str(lead.get("price", 0) or 0),
            "kommo_lead_id": str(lead_id),
        }
        doc_id = create_documenso_doc(signer_name, email, fields)
        save_doc_id(lead_id, doc_id)

        requests.post(
            f"{KOMMO_BASE}/notes",
            headers=KOMMO_HDR,
            json=[{
                "entity_id":   lead_id,
                "entity_type": "leads",
                "note_type":   "common",
                "params":      {"text": f"Documenso: документ {doc_id} отправлен на подпись {email}"},
            }],
        )

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

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

def verify_documenso_sig(body: bytes, sig: str) -> bool:
    computed = hmac.new(DS_WEBHOOK_SC.encode(), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(computed, sig)

@app.route("/webhooks/documenso", methods=["POST"])
def documenso_webhook():
    sig = request.headers.get("X-Documenso-Signature", "")
    if DS_WEBHOOK_SC and not verify_documenso_sig(request.data, sig):
        return jsonify({"error": "invalid signature"}), 401

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

    if ev_type not in ("document.completed", "document.declined"):
        return jsonify({"status": "ignored"}), 200

    doc    = event.get("data", {})
    doc_id = str(doc.get("id", ""))

    # Найти сделку по doc_id (ищем в formValues)
    meta   = doc.get("meta", {}) or {}
    fields = doc.get("formValues", {}) or meta.get("formValues", {}) or {}
    lead_id = fields.get("kommo_lead_id", "")

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

    if ev_type == "document.completed":
        # Скачать подписанный PDF URL
        r_doc = requests.get(f"{DOCUMENSO_BASE}/documents/{doc_id}", headers=DS_HDR)
        pdf_url = r_doc.json().get("downloadUrl", "") if r_doc.status_code == 200 else ""

        requests.patch(
            f"{KOMMO_BASE}/leads/{lead_id}",
            headers=KOMMO_HDR,
            json={"status_id": SIGNED_STAGE_ID},
        )
        requests.post(
            f"{KOMMO_BASE}/notes",
            headers=KOMMO_HDR,
            json=[{
                "entity_id":   int(lead_id),
                "entity_type": "leads",
                "note_type":   "common",
                "params":      {"text": f"Documenso: договор подписан. PDF: {pdf_url}"},
            }],
        )
    else:
        requests.post(
            f"{KOMMO_BASE}/notes",
            headers=KOMMO_HDR,
            json=[{
                "entity_id":   int(lead_id),
                "entity_type": "leads",
                "note_type":   "common",
                "params":      {"text": "Documenso: подписант отклонил документ. Уточните причину."},
            }],
        )

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

Self-hosted Documenso: Docker Compose

version: "3"
services:
  documenso:
    image: documenso/documenso:latest
    environment:
      NEXTAUTH_SECRET: "your-secret"
      NEXTAUTH_URL: "https://sign.yourcompany.com"
      DATABASE_URL: "postgresql://user:pass@postgres:5432/documenso"
      NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS: ""  # или путь к сертификату
    ports:
      - "3000:3000"
  postgres:
    image: postgres:15
    environment:
      POSTGRES_DB: documenso
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass

После запуска: DS_BASE=https://sign.yourcompany.com.

Стоимость vs DocuSign

Documenso CloudDocuSign Essentials
Цена~$30/mo (5 пользователей)$15/user/mo
APIДаДа (от Standard)
Self-hostedДа (бесплатно)Нет
Audit trailДаДа
eIDASВ разработкеДа

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

Компании, чувствительные к стоимости eSign-лицензий и имеющие возможность self-hosting. Особенно агентства и стартапы с 10+ контрактами в месяц. Если требуется полное соответствие eIDAS AES/QES - пока лучше Yousign или Scrive.

Аналогичная open-source интеграция описана для Kommo + Docuseal.

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

Documenso подписывает документы с временной меткой (timestamp)?

Да, Documenso поддерживает Qualified Electronic Timestamp (QTS) через интеграцию с TSA (Time Stamp Authority). При self-hosted нужно настроить TSA endpoint. В облачной версии временная метка включена автоматически.

Как настроить кастомный домен в Documenso Cloud?

Documenso Cloud Enterprise позволяет использовать кастомный домен для страницы подписи (sign.yourcompany.com). На self-hosted - настраивается через NEXTAUTH_URL. Брендинг: логотип и цвета настраиваются в Team Settings.

Работает ли formValues с существующими PDF-документами?

formValues работает только с документами, созданными через Documenso шаблон с явно заданными полями. Если нужно заполнить поля в существующем PDF, используйте Documenso Templates: создайте шаблон из PDF, добавьте Text-поля с именами полей, и они будут доступны через formValues API.

Итог

Kommo + Documenso - open-source eSign без вендор-локина:

  • Bearer token, POST /api/v1/templates/{id}/create-document + send
  • formValues.kommo_lead_id для обратной корреляции webhook
  • HMAC-SHA256 верификация X-Documenso-Signature
  • document.completed -> Closed Won + ссылка на PDF
  • Self-hosted: сменить DOCUMENSO_BASE, API идентичен

Если нужна интеграция Kommo с Documenso или другим open-source eSign - опишите задачу команде Exceltic.dev.

Ещё статьи

Все →