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
- Signeasy -> Settings -> API Access -> Generate API Token
- Templates -> create a contract template with fields
company_name,contract_amount,kommo_lead_id- The
kommo_lead_idfield should be Hidden - it is filled via the API and not shown to the signer
- The
- Webhooks -> Add webhook endpoint
- URL:
https://your-server.com/webhooks/signeasy - Events: signature_request.signed, signature_request.declined, signature_request.expired
- URL:
- 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_requestswithprefill_fields - Hidden field
kommo_lead_idfor reverse correlation - HMAC-SHA256 webhook verification (
X-Signeasy-Signature) signature_request.signed-> move the deal + add PDF linksignature_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.