Kommo + Skribble: квалифицированная электронная подпись EU/Швейцария из воронки

Skribble - швейцарская платформа электронной подписи с поддержкой Qualified Electronic Signature (QES) по стандартам EU eIDAS и Swiss ZertES. QES - единственный тип подписи, имеющий в EU такую же юридическую силу как рукописная. Это критично для договоров в Германии, Австрии, Швейцарии и Франции, где требования к электронным подписям выше чем в США или Великобритании.

Для B2B компаний, работающих в EU и подписывающих contract с корпоративными клиентами, Skribble + Kommo закрывает задачу: отправить документ на QES-подпись из карточки сделки, получить статус в Kommo при подписании, архивировать подписанный PDF.

Skribble API использует Bearer token. Основные операции: POST /v2/signature-requests - создать запрос на подпись, загрузить документ и добавить signers. Webhooks: signature-request-signed, signature-request-declined.

QES (Qualified Electronic Signature) - наивысший уровень электронной подписи по eIDAS. Требует идентификацию личности через видеозвонок с агентом или eMedId. Имеет такую же юридическую силу как рукописная подпись в 30+ европейских странах.

Уровни подписи в Skribble

УровеньСтандартЮридическая силаИдентификация
EES (Simple)eIDASБазоваяEmail
AES (Advanced)eIDASВысокаяSMS/2FA
QES (Qualified)eIDAS/ZertESМаксимальная (= рукопись)Видеоидентификация

Для большинства B2B договоров достаточно AES. QES нужна для юридически значимых документов: трудовые договоры, кредитные соглашения, некоторые M&A документы.

Архитектура

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

Ваш сервер
  -> Загрузить PDF шаблон / взять шаблон из хранилища
  -> Skribble API: POST /v2/signature-requests
     {title, content (base64 PDF), signers, signature_quality: "AES"}
  -> Kommo: записать signature_request_id

Клиент идентифицируется и подписывает
  -> Skribble webhook: signature-request-signed
  -> Ваш сервер -> Kommo: Closed Won + скачать PDF

Реализация

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

app = Flask(__name__)

SKRIBBLE_API_KEY = os.environ["SKRIBBLE_API_KEY"]
SKRIBBLE_BASE    = "https://api.skribble.com/v2"
SKRIBBLE_HDR     = {"Authorization": f"Bearer {SKRIBBLE_API_KEY}", "Content-Type": "application/json"}
SKRIBBLE_SECRET  = os.environ.get("SKRIBBLE_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_SR_ID    = int(os.environ["KOMMO_CF_SIGNATURE_REQUEST_ID"])

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

CONTRACT_PDF_PATH = os.environ.get("CONTRACT_TEMPLATE_PATH", "contract_template.pdf")

def get_lead_contact_email(lead_id: int) -> tuple[dict, str, str]:
    r = requests.get(
        f"{KOMMO_BASE}/leads/{lead_id}",
        headers=KOMMO_HDR,
        params={"with": "contacts"},
    )
    lead     = r.json()
    contacts = lead.get("_embedded", {}).get("contacts", [])
    if not contacts:
        return lead, "", ""
    rc = requests.get(
        f"{KOMMO_BASE}/contacts/{contacts[0]['id']}",
        headers=KOMMO_HDR,
        params={"with": "custom_fields_values"},
    )
    c     = rc.json()
    email = ""
    for cf in c.get("custom_fields_values", []) or []:
        if cf.get("field_code") == "EMAIL":
            vals = cf.get("values", [])
            if vals:
                email = vals[0].get("value", "")
                break
    return lead, c.get("name", ""), email

def create_signature_request(pdf_path: str, signer_name: str, signer_email: str,
                              title: str, lead_id: int) -> str:
    with open(pdf_path, "rb") as f:
        pdf_b64 = base64.b64encode(f.read()).decode()

    r = requests.post(
        f"{SKRIBBLE_BASE}/signature-requests",
        headers=SKRIBBLE_HDR,
        json={
            "title":     title,
            "message":   f"Пожалуйста, подпишите договор. Ссылка действительна 14 дней.",
            "content":   pdf_b64,
            "signers":   [{
                "email":           signer_email,
                "name":            signer_name,
                "signature_type":  "AES",  # или "QES" для квалифицированной
                "language":        "de",   # de/en/fr/it
            }],
            "meta": {"kommo_lead_id": str(lead_id)},
        },
    )
    r.raise_for_status()
    return r.json().get("id", "")

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

def add_note(lead_id: int, text: str):
    requests.post(
        f"{KOMMO_BASE}/notes",
        headers=KOMMO_HDR,
        json=[{
            "entity_id":   lead_id,
            "entity_type": "leads",
            "note_type":   "common",
            "params":      {"text": text},
        }],
    )

@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, name, email = get_lead_contact_email(lead_id)
        if not email:
            continue

        title = f"Договор - {name} - #{lead_id}"
        sr_id = create_signature_request(CONTRACT_PDF_PATH, name, email, title, lead_id)
        save_sr_id(lead_id, sr_id)
        add_note(lead_id, f"Skribble: запрос на подпись {sr_id} отправлен на {email}.")

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

@app.route("/webhooks/skribble", methods=["POST"])
def skribble_webhook():
    event    = request.json or {}
    ev_type  = event.get("event_type", "")

    meta     = event.get("signature_request", {}).get("meta", {}) or {}
    lead_id  = meta.get("kommo_lead_id", "")
    sr_id    = event.get("signature_request", {}).get("id", "")

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

    if ev_type == "signature-request-signed":
        # Скачать подписанный PDF
        r_pdf = requests.get(
            f"{SKRIBBLE_BASE}/signature-requests/{sr_id}/download",
            headers=SKRIBBLE_HDR,
        )
        pdf_url = ""
        if r_pdf.status_code == 200:
            save_path = f"signed_{lead_id}_{sr_id}.pdf"
            with open(save_path, "wb") as f:
                f.write(r_pdf.content)
            pdf_url = f"(PDF сохранён: {save_path})"

        requests.patch(
            f"{KOMMO_BASE}/leads/{lead_id}",
            headers=KOMMO_HDR,
            json={"status_id": SIGNED_STAGE_ID},
        )
        add_note(int(lead_id), f"Skribble: договор подписан. {pdf_url}")

    elif ev_type == "signature-request-declined":
        add_note(int(lead_id), "Skribble: подписант отклонил запрос на подпись. Уточните причину.")

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

Качество подписи: AES vs QES

Для большинства B2B договоров (NDA, сервисные соглашения, SaaS terms) достаточно signature_type: "AES". Клиент идентифицируется через SMS или email.

QES ("QES") требует предварительной видеоидентификации через Skribble ID или eMedId. Signer должен пройти идентификацию один раз - затем подписывает без дополнительных шагов. Используйте QES для трудовых договоров в Германии (§ 623 BGB), кредитных соглашений и документов требующих нотариальной заверки.

Языки и белый лейблинг

Skribble поддерживает language: "de"/"en"/"fr"/"it" для email-уведомлений подписанту. Enterprise план - кастомный брендинг страницы подписи. Для мультиязычных команд: определяйте язык из custom field контакта Kommo.

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

B2B компании с клиентами в DACH (Германия, Австрия, Швейцария), Франции, Скандинавии. Особенно если договоры требуют eIDAS-совместимой подписи для юридической силы в ЕС. Skribble - один из немногих провайдеров с нативной поддержкой Swiss ZertES (швейцарское законодательство об электронных подписях).

Аналогичные интеграции eSign: Kommo + Documenso (open-source), Kommo + Yousign (французская платформа).

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

Как Skribble хранит подписанные документы?

Skribble хранит подписанные PDF с встроенной цифровой подписью и audit trail. Документы доступны через API и Dashboard неограниченное время (на Enterprise плане). Дополнительно рекомендуется скачивать и хранить в собственном S3/хранилище - так вы не зависите от Skribble для доступа к архиву.

Можно ли подписывать документы с несколькими signers?

Да. В signers передайте список подписантов - Skribble отправит каждому персональную ссылку. Можно настроить порядок: последовательный (один за другим) или параллельный (все одновременно). Webhook signature-request-signed приходит когда ВСЕ signers подписали.

Как Skribble интегрируется с DATEV для немецких клиентов?

Skribble имеет нативный коннектор с DATEV DMS (Document Management System). Подписанные документы автоматически попадают в DATEV. Для российско-немецких B2B команд это упрощает работу с немецкими налоговыми консультантами. Настройка через Skribble Dashboard без API.

Итог

Kommo + Skribble - QES/AES подпись для EU из воронки:

  • Bearer token, POST /v2/signature-requests с base64 PDF и meta.kommo_lead_id
  • signature_type: "AES" для большинства договоров, "QES" для трудовых/кредитных
  • Webhook signature-request-signed -> download PDF -> Kommo Closed Won
  • Поддержка eIDAS (EU) и ZertES (Швейцария) - наивысшая юридическая сила
  • language: "de"/"fr"/"en" для мультиязычных команд

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

Ещё статьи

Все →