Kommo + Concord: Automatic Contract Sending from Your Pipeline
When a deal moves to “Won”, Kommo automatically creates a document from a template in Concord CLM, populates it with client data, and sends it for signing. The contract link and its status come back into the CRM card.
When closing a deal, the manager manually copies the company name, amount, details, and terms into a contract template in Concord, then sends it to the counterparty and waits. With 30-50 deals per month, that is about 3-4 hours of manual data entry and a constant source of errors: wrong amount, outdated template version, mixed-up details. One such contract with a mistake can delay deal closure by a week. Concord is a CLM platform (Contract Lifecycle Management) with support for templates, variables, and approval workflows. Its API allows documents to be created programmatically by passing variable values from external systems. This article covers the architecture of the two-way integration, Python code, and a B2B SaaS case study.
Why manual document workflow does not scale
Kommo has no native integration with Concord. Existing solutions via Zapier hit one key limitation: Zapier can trigger document creation in Concord, but cannot correctly map complex template variables (nested objects, conditional blocks) and does not handle the reverse webhook when signing is complete.
As a result, companies end up with semi-automation: the document is created automatically, but the manager still has to go into Concord, check the data, fix mapping errors, and only then send it. The value of automation drops to zero.
A custom API integration closes the full loop: document creation, sending, status tracking, and writing the result back to the CRM.
Integration architecture
A two-way data flow:
Kommo -> Concord (on deal won):
- Webhook from Kommo to your service
- Service fetches full deal and contact data via the Kommo API
- Creates a document from a template in Concord via
POST /1/documents - Writes the contract URL back into a custom Kommo field
Concord -> Kommo (on contract status change):
- Webhook from Concord on
document.signed,document.countersigned,document.expired - Service updates the “Contract Status” custom field in Kommo
- Adds a note with the signing date and participants
Concord API authentication: Bearer token (Authorization: Bearer <api_key>) from Settings -> API in your Concord account.
import httpx
from fastapi import FastAPI, Request, HTTPException
import logging
app = FastAPI()
logger = logging.getLogger(__name__)
CONCORD_API_KEY = "your_concord_api_key"
CONCORD_TEMPLATE_ID = "template_uuid_here" # template ID in Concord
KOMMO_SUBDOMAIN = "your_company"
KOMMO_TOKEN = "your_kommo_token"
# Custom field IDs in Kommo (find via GET /api/v4/leads/custom_fields)
FIELD_CONTRACT_URL = 123456
FIELD_CONTRACT_STATUS = 123457
async def get_kommo_deal(deal_id: int) -> dict:
"""Fetches deal data and the associated contact."""
async with httpx.AsyncClient() as client:
resp = await client.get(
f"https://{KOMMO_SUBDOMAIN}.kommo.com/api/v4/leads/{deal_id}",
headers={"Authorization": f"Bearer {KOMMO_TOKEN}"},
params={"with": "contacts"},
timeout=10.0
)
resp.raise_for_status()
return resp.json()
async def get_kommo_contact(contact_id: int) -> dict:
"""Fetches contact data (email, phone, company)."""
async with httpx.AsyncClient() as client:
resp = await client.get(
f"https://{KOMMO_SUBDOMAIN}.kommo.com/api/v4/contacts/{contact_id}",
headers={"Authorization": f"Bearer {KOMMO_TOKEN}"},
timeout=10.0
)
resp.raise_for_status()
return resp.json()
async def create_concord_document(deal_data: dict, contact_data: dict) -> dict:
"""
Creates a document from a template in Concord.
Maps template variables from Kommo fields.
"""
# Extract required fields from Kommo data
custom_fields = {cf["field_id"]: cf["values"][0]["value"]
for cf in deal_data.get("custom_fields_values", [])}
# Variables for the Concord template
template_variables = {
"client_name": contact_data.get("name", ""),
"company_name": contact_data.get("company", {}).get("name", ""),
"client_email": next(
(v["value"] for v in contact_data.get("custom_fields_values", [])
if v.get("field_type") == "EMAIL"), ""
),
"deal_amount": str(deal_data.get("price", 0)),
"deal_name": deal_data.get("name", ""),
"deal_id": str(deal_data.get("id", "")),
# Additional fields from Kommo custom fields:
"contract_start_date": custom_fields.get(111111, ""),
"subscription_term": custom_fields.get(111112, "12 months"),
}
async with httpx.AsyncClient() as client:
resp = await client.post(
"https://api.concord.app/1/documents",
headers={
"Authorization": f"Bearer {CONCORD_API_KEY}",
"Content-Type": "application/json"
},
json={
"template_id": CONCORD_TEMPLATE_ID,
"title": f"Contract - {deal_data.get('name', '')} #{deal_data.get('id')}",
"variables": template_variables,
"send_for_signature": True # automatically send for signing
},
timeout=15.0
)
resp.raise_for_status()
return resp.json()
async def update_kommo_deal_fields(deal_id: int, contract_url: str, status: str):
"""Updates custom deal fields in Kommo."""
async with httpx.AsyncClient() as client:
await client.patch(
f"https://{KOMMO_SUBDOMAIN}.kommo.com/api/v4/leads/{deal_id}",
headers={"Authorization": f"Bearer {KOMMO_TOKEN}"},
json={
"custom_fields_values": [
{"field_id": FIELD_CONTRACT_URL, "values": [{"value": contract_url}]},
{"field_id": FIELD_CONTRACT_STATUS, "values": [{"value": status}]}
]
},
timeout=10.0
)
@app.post("/kommo/webhook")
async def handle_kommo_webhook(request: Request):
"""Handles deal transition to Won and creates a contract in Concord."""
data = await request.json()
for lead in data.get("leads", {}).get("status", []):
if lead.get("status_id") == 142: # Won
deal_id = lead["id"]
deal_data = await get_kommo_deal(deal_id)
# Take the first associated contact
contact_id = deal_data["_embedded"]["contacts"][0]["id"]
contact_data = await get_kommo_contact(contact_id)
doc = await create_concord_document(deal_data, contact_data)
contract_url = doc.get("document_url", "")
await update_kommo_deal_fields(deal_id, contract_url, "Sent")
logger.info(f"Contract created for deal {deal_id}: {contract_url}")
return {"status": "ok"}
@app.post("/concord/webhook")
async def handle_concord_webhook(request: Request):
"""Handles contract signing events from Concord."""
event = await request.json()
event_type = event.get("event")
document = event.get("document", {})
# Extract deal_id from document variables
variables = document.get("variables", {})
deal_id_str = variables.get("deal_id")
if not deal_id_str:
return {"status": "skipped"}
deal_id = int(deal_id_str)
status_map = {
"document.signed": "Signed",
"document.countersigned": "Countersigned",
"document.expired": "Expired",
}
new_status = status_map.get(event_type, "Unknown")
contract_url = document.get("document_url", "")
await update_kommo_deal_fields(deal_id, contract_url, new_status)
return {"status": "ok"}
Step-by-step implementation
Step 1. Prepare the template in Concord
In Concord, create or adapt a contract template. Variables use double curly braces: {{client_name}}, {{deal_amount}}, {{contract_start_date}}. Verify that the template works with a manual test. Save the template UUID from the URL or via the API.
Step 2. Map Kommo fields
Determine which deal and contact fields are needed to populate the template. Retrieve the list of custom fields via GET /api/v4/leads/custom_fields. Standard deal fields (name, price, status) are available directly.
Step 3. Webhook from Kommo
Configure a webhook in Kommo for the pipeline stage change event. Use the specific pipeline_id and status_id of the “Won” stage to avoid processing unnecessary events.
Step 4. Webhook in Concord
Following the Concord documentation, set up a webhook for document events. Enter your service URL and subscribe to document.signed, document.countersigned, document.expired.
Step 5. Store deal_id in document variables
Key pattern: pass deal_id as a template variable (even if it does not appear in the contract text). This allows the reverse webhook to accurately identify which deal to update.
Step 6. Error handling
The Concord API may return 422 if a template variable is empty or the template is not found. Log the error details and send a notification to Slack/email - this lets you fix the mapping quickly without losing the deal.
Real case: B2B SaaS, 35 deals per month
Client - a B2B SaaS company with a sales team of 8 people in Europe. Before the integration: the AE closes the call, switches to Concord, manually creates a document from the template, fills in 7-8 variables, checks the data, and sends it. Average time - 15-20 minutes per contract. Errors (wrong amount, outdated template version) - 2-3 per month.
After the integration: when the stage changes in Kommo, the contract is created and sent to the client automatically within 5-10 seconds. The AE sees the link in the card, and the “Sent” status changes to “Signed” automatically.
Results:
- Time saved: ~9 hours per month (35 deals x 15 min)
- Data errors in contracts: dropped to zero
- Signing speed improved: the client receives the contract while the mood is positive, not an hour later
- The sales director can see in Kommo a list of deals with unsigned contracts - previously this required manual reconciliation
The company additionally set up a trigger: if a contract is not signed within 48 hours, Kommo automatically creates a task for the manager with a “follow up” note.
Who this integration is right for
- Companies with 20+ deals per month where each requires a contract
- B2B SaaS, agencies, consulting - where the contract template is standardized
- Teams where the AE closes the deal and also handles the paperwork
- Situations with multiple signatories: Concord supports sequential and parallel signing
If you already use DocuSign or Dropbox Sign, Concord offers similar functionality with a focus on contract lifecycle management and built-in approval tools.
Frequently asked questions
Does Concord support legally valid signatures in the EU (eIDAS)?
Concord provides an electronic signature that meets eIDAS requirements under the Simple Electronic Signature (SES) category. For most B2B contracts, this is sufficient. If you need an Advanced or Qualified Electronic Signature, consider specialized EU platforms (Yousign, Docuseal with EU Cloud). Check the current compliance status in the Concord documentation before making a final decision.
How do I pass data from multiple contacts (for example, two signatories) into the contract?
The Concord API allows you to specify a list of signatories with email addresses when creating a document via the signers field. In Kommo, create custom fields for the second signatory contact and pass them in the API request body. Signing order (sequential or parallel) is configured via the signing_order parameter.
What if the deal was moved to Won by mistake and the contract needs to be revoked?
Concord allows you to revoke a document via the API (DELETE /1/documents/{id} or through the dashboard). We recommend adding a 2-5 minute delay between the Kommo trigger and contract creation - this gives the manager time to notice the mistake. You can also add a custom flag field “Do not create contract automatically” for exceptional cases.
Can different templates be used for different types of deals?
Yes. In Kommo, create a custom field “Contract Type” or use the pipeline/stage name to select the appropriate template_id in Concord. The template selection logic is implemented on the webhook service side as a simple dictionary {deal_type: template_uuid}.
How do I test the integration without sending real contracts to clients?
Concord supports a test mode via a sandbox account. In Kommo, create a test pipeline or a test stage for QA. The parameter send_for_signature: false allows you to create a document without immediately sending it - useful for verifying data mapping.
If your team spends 10+ hours per month filling in contracts manually - describe the task to the Exceltic.dev team. This is a typical task for us: we will review your stack, template mapping, and propose a solution.