Kommo + Ironclad: Launching a Contract from the CRM Pipeline Without Manual Work

Ironclad is a CLM platform (Contract Lifecycle Management) with an API for creating and managing contracts programmatically. The Kommo integration solves a common problem: when a deal reaches the negotiation stage, you need to launch a contract with the right parameters - counterparty name, amount, date. Instead of manually switching to Ironclad and filling out a form, a single automated call fires when the pipeline stage changes in your CRM.

The Ironclad API uses a Bearer token (API Key from Ironclad Admin -> Integrations -> API). Key endpoints: GET /v1/templates - list workflow templates, POST /v1/workflows - launch a new contract, GET /v1/workflows/{workflowId} - check status. The workflow.state.changed webhook notifies you when a contract is signed (Executed) - and at that moment Kommo automatically moves to Closed Won.

CLM (Contract Lifecycle Management) is a class of tools for managing the full lifecycle of a contract: from creation and review to signing, storage, and renewal. Ironclad is one of the segment leaders, positioned as an enterprise solution with a workflow engine on top of simple eSign.

The Problem with Native Integration

Ironclad has no native connector to Kommo. The typical workaround - Zapier - does not work: Ironclad’s Zapier connector is limited and does not support signerGroups and attributes, which are required to correctly launch a workflow. Without these parameters, the contract is created as an empty template with no deal data.

Additionally, there is no feedback loop: even if you manage to create a workflow via Zapier, the “contract signed” event never reaches Kommo - the sales rep has no idea when to close the deal.

Architecture

Kommo: deal -> stage "Send Contract"
  -> Kommo webhook leads.status.changed
  -> Your server

Your server:
  -> GET /v1/templates -> find template by name
  -> GET Kommo: counterparty name, amount, signer email
  -> POST /v1/workflows -> workflowId
  -> Kommo: note with workflowId and link

Ironclad workflow: Draft -> Review -> Signature -> Executed
  -> webhook workflow.state.changed (state = "EXECUTED")
  -> Your server: find deal by workflowId
  -> Kommo: PATCH leads -> Closed Won

Launching the Workflow

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

app = Flask(__name__)

IRONCLAD_KEY    = os.environ["IRONCLAD_API_KEY"]
IRONCLAD_BASE   = "https://ironcladapp.com/public/api"
IRONCLAD_HDR    = {"Authorization": f"Bearer {IRONCLAD_KEY}",
                   "Content-Type": "application/json"}

KOMMO_SUBDOMAIN    = os.environ["KOMMO_SUBDOMAIN"]
KOMMO_TOKEN        = os.environ["KOMMO_ACCESS_TOKEN"]
CONTRACT_STAGE_ID  = int(os.environ["KOMMO_CONTRACT_STAGE_ID"])
CLOSED_WON_ID      = int(os.environ["KOMMO_CLOSED_WON_ID"])
TEMPLATE_NAME      = os.environ.get("IRONCLAD_TEMPLATE_NAME", "MSA")

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

# For storing workflowId -> leadId mapping (use a DB or Redis in production)
workflow_to_lead: dict = {}

def get_template_id(name: str) -> str:
    r = requests.get(f"{IRONCLAD_BASE}/v1/templates", headers=IRONCLAD_HDR)
    r.raise_for_status()
    for t in r.json().get("templates", []):
        if name.lower() in t.get("name", "").lower():
            return t["id"]
    raise ValueError(f"Template '{name}' not found")

def launch_workflow(template_id: str,
                    counterparty: str, amount: float,
                    signer_email: str, signer_name: str) -> dict:
    r = requests.post(
        f"{IRONCLAD_BASE}/v1/workflows",
        headers=IRONCLAD_HDR,
        json={
            "template": {"id": template_id},
            "attributes": {
                "counterpartyName": counterparty,
                "contractValue":    amount,
            },
            "signerGroups": [
                {
                    "group": "1",
                    "signers": [
                        {"email": signer_name, "name": signer_name}
                    ],
                }
            ],
        },
    )
    r.raise_for_status()
    return r.json()

def get_lead_contact(lead_id: int) -> tuple:
    r = requests.get(
        f"{KOMMO_BASE}/leads/{lead_id}",
        headers=KOMMO_HDR,
        params={"with": "contacts"},
    )
    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", [])
            return vals[0].get("value", "") if vals else ""
    return ""

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 != CONTRACT_STAGE_ID:
            continue

        lead, contact = get_lead_contact(lead_id)
        amount      = float(lead.get("price") or 0)
        company     = contact.get("name", f"Deal #{lead_id}")
        email       = get_email(contact)
        signer_name = contact.get("name", "")

        if not email:
            add_note(lead_id, "Ironclad: signer email not provided, please launch the contract manually.")
            continue

        template_id = get_template_id(TEMPLATE_NAME)
        wf          = launch_workflow(template_id, company, amount, email, signer_name)
        wf_id       = wf.get("id", "")

        workflow_to_lead[wf_id] = lead_id
        view_url = wf.get("viewerUrl", "")
        add_note(lead_id,
                 f"Ironclad workflow #{wf_id} launched. Status: Draft.\n{view_url}")

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

Webhook on Contract Signing

@app.route("/webhooks/ironclad", methods=["POST"])
def ironclad_webhook():
    event = request.json or {}
    if event.get("event") != "workflow.state.changed":
        return jsonify({"status": "ignored"}), 200

    wf_id    = event.get("workflowId", "")
    new_state = event.get("workflowStatus", "")

    if new_state != "EXECUTED":
        return jsonify({"status": "not_executed"}), 200

    lead_id = workflow_to_lead.get(wf_id)
    if not lead_id:
        return jsonify({"status": "no_lead"}), 200

    requests.patch(
        f"{KOMMO_BASE}/leads/{lead_id}",
        headers=KOMMO_HDR,
        json={"status_id": CLOSED_WON_ID},
    )
    add_note(lead_id, f"Ironclad contract #{wf_id} signed (Executed). Deal closed.")
    del workflow_to_lead[wf_id]
    return jsonify({"status": "ok"}), 200

Configuring the Webhook in Ironclad

In Ironclad Admin -> Integrations -> Webhooks: add the URL https://your-server.com/webhooks/ironclad and select the workflow.state.changed event. Ironclad signs requests via HMAC-SHA256 (secret from webhook settings) in the X-Ironclad-Signature header.

For production: add signature verification - this is a standard HMAC-SHA256 of the body using the key from the webhook settings.

Ironclad Workflow States

StatusMeaning
CREATEDWorkflow just created
DRAFTTeam is filling in fields
REVIEWContract under legal review
SIGNATURESent to signers
EXECUTEDAll parties have signed
CANCELLEDCancelled

For most sales use cases, SIGNATURE (remind the rep that we are waiting for a signature) and EXECUTED (close the deal in the CRM) are the relevant states.

Real-World Case

A B2B SaaS company with enterprise clients: average deal cycle 45 days, contract review takes 7-14 days. Before the integration: the rep manually launched the Ironclad workflow and two weeks later manually closed the deal in Kommo. After: the workflow launches automatically on a stage change, and the CRM updates when the contract is signed. Time saved - 15 minutes per deal, 0 forgotten “close the deal after signing” tasks.

Who This Is For

B2B companies with enterprise clients and legal contract review processes. Especially relevant for SaaS with MSA/NDA flows, professional services with Statements of Work, and any business where 2+ weeks of review pass between “agreed” and “signed.” Kommo + Ironclad closes the gap between the CRM sales process and the CLM legal execution process.

Other document workflow integrations: Kommo + Skribble (eIDAS/QES signature), Kommo + Documenso (open source eSign).

Frequently Asked Questions

Does the Ironclad API support contract versioning?

Yes. Every change to a contract creates a new version. You can retrieve the version history via API: GET /v1/workflows/{workflowId}/revisions. The workflow.state.changed webhook delivers the current version.

Can I retrieve the signed PDF via API?

Yes: GET /v1/workflows/{workflowId}/documents returns a list of documents, each with a downloadUrl. The PDF is available after the workflow transitions to EXECUTED.

How do I pass custom fields from Kommo to Ironclad?

Via attributes in the POST /v1/workflows request body. Keys must match the field names in the Ironclad template. To find valid keys: GET /v1/templates/{templateId} returns a list of schemaFields with their types and names.

Does Ironclad have a sandbox for testing?

Yes: sandbox.ironcladapp.com. Create a separate API key for the sandbox in Ironclad Admin -> Integrations -> API. Webhook events from the sandbox arrive the same way as from production.

Summary

Kommo + Ironclad CLM - automated contract flow:

  • Bearer token, POST /v1/workflows with template, attributes, signerGroups
  • Store workflowId -> leadId mapping (in-memory or Redis)
  • Webhook workflow.state.changed, state EXECUTED -> Kommo Closed Won
  • States: CREATED -> DRAFT -> REVIEW -> SIGNATURE -> EXECUTED
  • Sandbox: sandbox.ironcladapp.com for testing without real contracts

If your team uses Ironclad for enterprise contracts and wants to automate the flow with Kommo - describe your use case to the Exceltic.dev team. We will work through the architecture for your specific stack.

More articles

All →