Kommo + Docuseal: automatic document signing via open-source platform

Kommo + Docuseal: automatic document signing via open-source platform

Docuseal is an open-source electronic signature platform: self-hosted or cloud, without vendor lock-in and with a full REST API. For teams that want e-sign without $25+/user/month for DocuSign or Adobe Sign, Docuseal is a functional alternative with complete API access. Without the Kommo integration, a manager creates a submission manually after Won. With the integration, Won automatically sends a contract from a template populated with deal data — the client receives a signing link within a minute.

Open-source vs SaaS for e-sign

Docuseal self-hosted (Docker) provides: — Full data control — all documents on your own server — Unlimited documents without plan limits — EU GDPR compliance without sending data to US SaaS — One-time hosting costs instead of $25–40/user/month

Docuseal cloud (docuseal.co) — the same API, without infrastructure management. More convenient for small teams.

Both options use the same REST API. For custom Kommo integration this means: one codebase, easy to switch between self-hosted and cloud.

What gets synchronized

Kommo -> Docuseal: — Won -> create submission from template with deal fields (name, email, amount, plan) — Won -> set signing order (client -> internal signature) — Automatically send email with signing link

Docuseal -> Kommo:submission.completed -> Note: “Docuseal: document signed by all parties” + stage change — submission.declined -> Note + task: “Client declined signing” — submission.expired -> Note + task: “Signing period expired” — form.completed (one signer completed) -> Note: “{name} signed”

Architecture

Kommo Webhook: deal moved to Won
  ↓ Backend
  1. GET /api/v4/leads/{id} + contacts
     -> email, name, plan, amount
  2. Docuseal API: POST /api/submissions
     -> template_id, submitters with deal fields
  3. Kommo: PATCH /leads/{id}
     -> docuseal_submission_id = submission.id
  4. Kommo: POST /leads/{id}/notes
     -> "Docuseal: contract sent for signature {email}"

Docuseal Webhook: submission.completed
  ↓ Backend
  1. Verify X-Docuseal-Signature
  2. Find deal by docuseal_submission_id
  3. Kommo: PATCH /leads/{id} -> stage change to "Contract signed"
  4. Kommo: POST /notes -> "Docuseal: all signatures collected"

Docuseal API: key requests

Base URL (cloud): https://api.docuseal.co. Base URL (self-hosted): https://your-docuseal-domain.com. Authentication: X-Auth-Token: {api_key} (from account settings).

Create a submission from a template:

import requests

DOCUSEAL_API_KEY = "your_api_key"
DOCUSEAL_BASE_URL = "https://api.docuseal.co"  # or self-hosted URL

headers = {
    "X-Auth-Token": DOCUSEAL_API_KEY,
    "Content-Type": "application/json"
}

def create_submission(template_id: int, submitters: list,
                      send_email: bool = True) -> dict:
    resp = requests.post(
        f"{DOCUSEAL_BASE_URL}/api/submissions",
        headers=headers,
        json={
            "template_id": template_id,
            "send_email": send_email,
            "submitters": submitters
        }
    )
    resp.raise_for_status()
    return resp.json()

def on_deal_won(lead: dict, contact: dict):
    email = get_contact_email(contact)
    name = contact["name"]
    plan = get_custom_field(lead, PLAN_FIELD_ID) or "Growth"
    amount = lead.get("price", 0)

    # submitters - order: client first, then internal signature
    submitters = [
        {
            "email": email,
            "name": name,
            "role": "Client",
            "values": {
                "client_name": name,
                "client_email": email,
                "plan": plan,
                "contract_amount": str(amount),
                "contract_date": datetime.now().strftime("%d.%m.%Y"),
            }
        },
        {
            "email": INTERNAL_SIGNER_EMAIL,
            "name": INTERNAL_SIGNER_NAME,
            "role": "Company",
        }
    ]

    submission = create_submission(DOCUSEAL_TEMPLATE_ID, submitters)
    submission_id = submission[0]["submission_id"]  # returns list of submitters

    update_kommo_deal(lead["id"], {"docuseal_submission_id": str(submission_id)})
    create_kommo_note(lead["id"],
        f"Docuseal: contract #{submission_id} sent for signature -> {email}")

Get submission status:

def get_submission_status(submission_id: int) -> dict:
    resp = requests.get(
        f"{DOCUSEAL_BASE_URL}/api/submissions/{submission_id}",
        headers=headers
    )
    resp.raise_for_status()
    return resp.json()

Handling Docuseal Webhooks:

import hmac, hashlib
from flask import Flask, request, abort

app = Flask(__name__)
DOCUSEAL_WEBHOOK_SECRET = "your_webhook_secret"

def verify_signature(payload: bytes, sig_header: str) -> bool:
    expected = hmac.new(
        DOCUSEAL_WEBHOOK_SECRET.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, sig_header)

@app.route("/webhooks/docuseal", methods=["POST"])
def docuseal_webhook():
    sig = request.headers.get("X-Docuseal-Signature", "")
    if not verify_signature(request.data, sig):
        abort(403)

    payload = request.json
    event = payload.get("event_type")
    submission_id = str(payload.get("data", {}).get("submission_id", ""))

    deal_id = find_deal_by_field("docuseal_submission_id", submission_id)
    if not deal_id:
        return "", 200

    if event == "submission.completed":
        update_kommo_deal(deal_id, {"stage_id": STAGE_CONTRACT_SIGNED})
        create_kommo_note(deal_id,
            "Docuseal: all signatures collected - document finalized")

    elif event == "submission.declined":
        create_kommo_note(deal_id, "Docuseal: client declined signing")
        create_kommo_task(deal_id, "Discuss terms - Docuseal recorded a refusal")

    elif event == "submission.expired":
        create_kommo_note(deal_id, "Docuseal: signing period expired")
        create_kommo_task(deal_id, "Send a new signing link")

    elif event == "form.completed":
        submitter_name = payload.get("data", {}).get("submitter", {}).get("name", "")
        create_kommo_note(deal_id, f"Docuseal: {submitter_name} signed")

    return "", 200

Webhook setup in Docuseal: Settings -> Webhooks -> Add Webhook. Specify URL and secret. Events: submission.completed, submission.declined, submission.expired, form.completed.

Self-hosted: setup in 5 minutes

# docker-compose.yml
version: '3'
services:
  docuseal:
    image: docuseal/docuseal:latest
    ports:
      - "3000:3000"
    volumes:
      - ./data:/data
    environment:
      - SECRET_KEY_BASE=your_secret_key
      - DATABASE_URL=sqlite3:///data/docuseal.sqlite3

docker compose up -d -> Docuseal available on port 3000. For production: Nginx reverse proxy + SSL + PostgreSQL instead of SQLite.

Real case

Law firm (EU, 50–70 contracts per month, Kommo + Docuseal self-hosted):

  • Before: DocuSign $40/user/month × 8 people = $320/month. Templates were configured manually, data from Kommo was copied. Sending delay 30–90 minutes.
  • After: Docuseal self-hosted on VPS at €20/month. Won -> contract with populated data in 8 seconds. Saving €3,620/year.
  • Additionally: EU GDPR — all documents on their own server, not transferred to US SaaS. A strong argument when working with EU clients.

Who should use this

  • Teams with GDPR requirements for document storage (legal, medical, financial)
  • Those who want to control costs — self-hosted eliminates the per-user-per-month fee
  • Developers who need an open API without proprietary limitations
  • 20+ contracts per month — at lower volumes manual work is still justified

Frequently asked questions

Docuseal vs DocuSign: is it realistic for enterprise?

Docuseal covers 80% of enterprise use cases: templates, signing order, audit log, signed PDF download. What is missing: advanced workflow (conditional routing), native Salesforce integration, enterprise SSO (on roadmap). For teams up to 50 people — sufficient.

What template formats does Docuseal support?

PDF with fillable fields. Templates can be created directly in the Docuseal UI (drag-and-drop fields onto a PDF). The API takes template_id — the template ID from Docuseal. One template can be used multiple times with different data via the API.

Is Docuseal legally valid in the EU?

Yes. Signatures via Docuseal comply with eIDAS (EU Electronic Signatures Regulation) as a Simple Electronic Signature (SES). For a Qualified Electronic Signature (QES) you need a separate identity provider — Docuseal does not generate one.

How do you upload templates via API?

POST /api/templates with multipart/form-data — upload a PDF file. The response contains the template id for use in subsequent submissions. Templates can also be created via UI — the API uses the same ID.

Summary

  • Docuseal: X-Auth-Token header, https://api.docuseal.co (cloud) or self-hosted URL
  • Create submission: POST /api/submissions with template_id and array of submitters
  • submitters[].values — auto-populate template fields with Kommo data
  • Webhook: HMAC-SHA256 via X-Docuseal-Signature
  • Self-hosted: Docker, data on your server, EU GDPR without issues
  • Key events: submission.completed, submission.declined, form.completed

If you want to automate contract signing from Kommo via Docuseal — describe your template structure and signing order. Exceltic.dev will configure the integration.

More articles

All →