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
| Status | Meaning |
|---|---|
| CREATED | Workflow just created |
| DRAFT | Team is filling in fields |
| REVIEW | Contract under legal review |
| SIGNATURE | Sent to signers |
| EXECUTED | All parties have signed |
| CANCELLED | Cancelled |
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/workflowswith 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.