Kommo + Signeasy: Electronic Document Signing from the Deal Card

Kommo + Signeasy: Electronic Document Signing from the Deal Card

Signeasy is an electronic signature platform with support for eIDAS (EU), the ESIGN Act (US), and the IT Act (India). It is positioned as a simpler and more affordable alternative to DocuSign for small and mid-sized businesses. It supports templates, fillable fields, and both sequential and parallel signing. A custom integration with Kommo solves the core problem of native eSign connectors: the envelope gets created and signed, but the status never makes it back into the deal card.

The Signeasy REST API uses Bearer token authentication. Key operations: create a signature request from a template, track status, and receive a signing notification via webhook.

A Signeasy Template is a pre-built document with fillable fields (name, date, amount). When creating a signature request, the fields are populated with data from the CRM deal.

What Breaks Without the Integration

The standard process without integration: a manager downloads the template -> manually fills in the client’s data -> uploads it to Signeasy -> sends it for signing -> tracks the status outside of Kommo.

Two friction points: deal data does not flow into the document automatically, and signing status does not come back into Kommo.

Architecture

Kommo: deal -> stage "Send Contract"
  -> Kommo webhook -> Your server

Your server
  -> Fetch deal data (name, email, amount)
  -> Signeasy API: POST /v1/signature_requests
     {template_id, signer.email, signer.name, fields: {amount, company}}
  -> Kommo: write signeasy_request_id to custom field

Client signs -> Signeasy
  -> Signeasy webhook: signature_request.signed
  -> Your server
  -> Kommo: move deal -> next stage
  -> Kommo: add link to signed PDF

Implementation: Sending for Signature

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":      "Please sign the contract.",
        "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

Implementation: Webhook on Signing

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

    # Find kommo_lead_id in 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", "")
        # Move the deal to the next stage
        requests.patch(
            f"{KOMMO_BASE}/leads/{lead_id}",
            headers=KOMMO_HDR,
            json={"status_id": SIGNED_STAGE_ID},
        )
        # Add link to the signed 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: contract signed. PDF: {signed_url}"},
            }],
        )
    elif event_type == "signature_request.declined":
        decliner = event.get("signer", {}).get("name", "client")
        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: contract declined by signer {decliner}. Please follow up to find out why."},
            }],
        )

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

Signeasy Configuration

  1. Signeasy -> Settings -> API Access -> Generate API Token
  2. Templates -> create a contract template with fields company_name, contract_amount, kommo_lead_id
    • The kommo_lead_id field should be Hidden - it is filled via the API and not shown to the signer
  3. Webhooks -> Add webhook endpoint
    • URL: https://your-server.com/webhooks/signeasy
    • Events: signature_request.signed, signature_request.declined, signature_request.expired
  4. Copy the Webhook Secret for HMAC verification

eIDAS (EU) Support

Signeasy supports Simple Electronic Signature (SES), which meets eIDAS requirements for most B2B contracts in the EU. For contracts that require Advanced Electronic Signature (AES) or Qualified Electronic Signature (QES), you will need providers on the EU Trust List (Yousign, Scrive).

Real-World Case

A SaaS company with 8 account executives and 30 contracts per month. Before the integration: managers spent 20 minutes per contract (copying data from Kommo -> Word -> PDF -> Signeasy). After: the signature request is created automatically when the deal is moved to the “Send Contract” stage. Time saved: 10 hours per month across the team.

Who This Is For

B2B SaaS and services companies with a 15-60 day sales cycle, contracts worth $1,000+, and a sales team of 3-20 people. Especially relevant when document flow is a bottleneck - signing delays hold back revenue recognition.

A similar approach is described for Kommo + DocuSign and Kommo + Dropbox Sign.

Frequently Asked Questions

How does Signeasy differ from DocuSign and Adobe Sign?

Signeasy is cheaper ($8-24/user/mo vs $15-45 for DocuSign) and easier to set up via API. It does not have native integrations with HubSpot or Salesforce out of the box - only through the API or Zapier. For smaller teams (up to 20 people) and straightforward contracts, it is a fully capable alternative.

How do you add multiple signers (sequential signing)?

Pass an array in signers: [{name, email, role, signing_order: 1}, {name, email, role, signing_order: 2}]. The second signer receives the request only after the first one has signed. The signature_request.signed webhook fires when all signers have completed.

Can you download the signed PDF via the API?

Yes: GET /v1/signature_requests/{id}/download returns the signed PDF. You can then upload it to Kommo as an attachment via the Kommo Files API. This is useful for archiving - the signed document lives directly inside the deal card.

Summary

Kommo + Signeasy - document workflow automation inside the sales pipeline:

  • Kommo webhook leads.status.changed -> POST /v1/signature_requests with prefill_fields
  • Hidden field kommo_lead_id for reverse correlation
  • HMAC-SHA256 webhook verification (X-Signeasy-Signature)
  • signature_request.signed -> move the deal + add PDF link
  • signature_request.declined -> create a task for the manager

If you need a Kommo integration with Signeasy or another eSign tool, reach out to Exceltic.dev.

More articles

All →