Kixie sends data for every call and SMS via webhook to your service. The service finds the contact in Kommo by phone number, then creates a note with the recording, transcript, and duration. There is no native Kixie widget for Kommo - only a custom integration.
The SDR uses Kixie for outreach: power dialer, AI transcription, SMS after the call. But all of that data stays only in Kixie. When the AE picks up the lead, they open Kommo and see an empty card - no call history, no transcripts, no dial attempts. They have to go into Kixie separately, search by number, and copy things over. At 200+ calls per day this becomes a bottleneck: context is lost, lead handoffs slow down, managers duplicate work. Kixie supports webhook integration with any CRM via a universal endpoint - and that is exactly what is used to connect with Kommo. This article covers the architecture, Python code, and an SDR team case study with numbers.
Why there is no native Kixie integration with Kommo
Kixie has native integrations with HubSpot, Salesforce, Pipedrive, and several other CRMs. Kommo is not on that list. The reason is straightforward: Kixie prioritizes the US market, where Kommo has less presence.
Trying to solve this through Zapier hits limitations: Zapier cannot search for a contact in Kommo by phone number in E.164 format, cannot correctly create a “note with attachment” containing a recording_url, and does not support batch operations at high call volume (200+ per day means 200+ Zap operations, plus the cost).
A custom webhook handler solves all of this at a fixed infrastructure cost.
E.164 is the international phone number format: +[country code][number], for example +12025551234. Kommo stores numbers in this format, and Kixie transmits them the same way - this is important for accurate contact lookup.
Integration architecture
Data flow is one-directional: Kixie -> your service -> Kommo.
On call completion (event: call.completed):
- Kixie sends a payload with: caller number, recipient number, duration, recording_url, transcript (if AI is enabled)
- Service looks up the contact in Kommo by phone number
- If found - adds a note with call details and a link to the recording
- If not found - creates a new contact and deal (optional)
On SMS received/sent (events: sms.received, sms.sent):
- Kixie sends a payload with the number and message body
- Service adds the SMS as a note to the corresponding contact card in Kommo
Kixie authentication: API Key passed as a query parameter or in the Authorization header. The webhook endpoint is configured in Kixie Dashboard -> Manage -> Webhooks.
import httpx
from fastapi import FastAPI, Request
from typing import Optional
import logging
app = FastAPI()
logger = logging.getLogger(__name__)
KOMMO_SUBDOMAIN = "your_company"
KOMMO_TOKEN = "your_kommo_token"
def normalize_phone(phone: str) -> str:
"""Normalizes phone number to E.164 for Kommo lookup."""
digits = "".join(filter(str.isdigit, phone))
if not digits.startswith("1") and len(digits) == 10:
digits = "1" + digits # US number without country code
return "+" + digits
async def find_kommo_contact(phone: str) -> Optional[dict]:
"""Looks up a contact in Kommo by phone number."""
normalized = normalize_phone(phone)
async with httpx.AsyncClient() as client:
resp = await client.get(
f"https://{KOMMO_SUBDOMAIN}.kommo.com/api/v4/contacts",
headers={"Authorization": f"Bearer {KOMMO_TOKEN}"},
params={"query": normalized},
timeout=10.0
)
if resp.status_code == 204:
return None # contact not found
resp.raise_for_status()
data = resp.json()
contacts = data.get("_embedded", {}).get("contacts", [])
return contacts[0] if contacts else None
async def add_kommo_note(contact_id: int, note_text: str, lead_id: Optional[int] = None):
"""Adds a note to a contact or deal in Kommo."""
entity_type = "leads" if lead_id else "contacts"
entity_id = lead_id or contact_id
async with httpx.AsyncClient() as client:
await client.post(
f"https://{KOMMO_SUBDOMAIN}.kommo.com/api/v4/{entity_type}/{entity_id}/notes",
headers={"Authorization": f"Bearer {KOMMO_TOKEN}"},
json=[{
"note_type": "common",
"params": {"text": note_text}
}],
timeout=10.0
)
async def get_contact_lead(contact_id: int) -> Optional[int]:
"""Finds the active deal for a contact."""
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}"},
params={"with": "leads"},
timeout=10.0
)
resp.raise_for_status()
data = resp.json()
leads = data.get("_embedded", {}).get("leads", [])
return leads[0]["id"] if leads else None
@app.post("/kixie/webhook")
async def handle_kixie_webhook(request: Request):
"""Handles all webhook events from Kixie."""
data = await request.json()
event_type = data.get("event_type", "")
# Determine the phone number to search by
if event_type == "call.completed":
# For inbound calls use caller, for outbound use recipient
direction = data.get("direction", "outbound")
phone = data.get("from_number") if direction == "inbound" else data.get("to_number")
elif event_type in ("sms.received", "sms.sent"):
direction = "inbound" if event_type == "sms.received" else "outbound"
phone = data.get("from_number") if direction == "inbound" else data.get("to_number")
else:
return {"status": "skipped", "event": event_type}
contact = await find_kommo_contact(phone)
if not contact:
logger.warning(f"Contact not found for phone: {phone}")
return {"status": "contact_not_found"}
contact_id = contact["id"]
lead_id = await get_contact_lead(contact_id)
# Build the note text
if event_type == "call.completed":
duration_sec = data.get("duration", 0)
duration_fmt = f"{duration_sec // 60}:{duration_sec % 60:02d}"
recording_url = data.get("recording_url", "")
transcript = data.get("transcript", "")
disposition = data.get("disposition", "")
note_parts = [
f"Kixie call [{direction}] - {duration_fmt}",
f"Disposition: {disposition}" if disposition else "",
f"Recording: {recording_url}" if recording_url else "",
f"Transcript:\n{transcript[:2000]}" if transcript else "" # limit length
]
note_text = "\n".join(p for p in note_parts if p)
elif event_type in ("sms.received", "sms.sent"):
sms_body = data.get("body", "")
arrow = "<-" if event_type == "sms.received" else "->"
note_text = f"Kixie SMS [{direction} {arrow}]:\n{sms_body}"
await add_kommo_note(contact_id, note_text, lead_id)
return {"status": "ok", "contact_id": contact_id}
Step-by-step implementation
Step 1. Configure the webhook in Kixie
In Kixie Dashboard -> Manage -> Webhooks, create a new webhook. Enter your service URL (https://your-service.com/kixie/webhook). Select events: End Call, SMS Inbound, SMS Outbound. If you have AI transcription enabled (Kixie CI), also add the CI Summary webhook.
Alternatively: configure via the API with POST https://apig.kixie.com/app/v1/api/postwebhook.
Step 2. Phone number normalization
Kixie may transmit numbers in different formats depending on settings. Always normalize to E.164 before searching in Kommo. The Kommo API supports partial-match search via the query parameter, but E.164 gives the most accurate result.
Step 3. Contact lookup and creation logic
If the contact is not found, decide in advance: create a new contact and deal automatically, or only log the event. For SDR teams handling inbound calls, we recommend auto-creation. For outbound dialing against an existing database, contacts are usually already in the CRM.
Step 4. AI transcript and CI Summary
Kixie CI (Conversation Intelligence) sends an additional ci_summary webhook 2-5 minutes after the call - after AI processing is complete. Set up a separate handler or use the same endpoint with different event_type values. Add the transcript as a separate note labeled “AI Summary”.
Step 5. Handling autodial sessions
When using Power Dialer, Kixie generates a sequence of events for each attempt. Filter by disposition: log only connected calls as the primary note; other attempts (busy, no_answer, voicemail) as abbreviated entries. This prevents the card from getting cluttered.
Step 6. Kommo API rate limits
Kommo limits request volume: 7 req/sec for most endpoints. With 200+ calls per day and simultaneous autodialing, spikes are possible. Use a queue (asyncio.Queue or Redis + Celery) and exponential backoff when receiving 429.
Real case: SDR team, 250 calls per day
Client - a B2B SaaS company with a team of 8 SDRs in Europe. Using Kixie Power Dialer for outbound dialing. Kommo is the primary CRM where AEs manage deals.
Before the integration: after each call the SDR manually opened Kommo, found the card, and wrote a note. Or (more often) did not, because there was no time. The AE received the lead and had no context from the calls.
After the integration: every call longer than 30 seconds automatically appears in Kommo as a note containing: duration, disposition (connected / voicemail / no_answer), link to the recording, and AI transcript (if a conversation occurred).
Results after the first month:
- 100% coverage: all calls are logged in the CRM without exception
- SDRs saved ~45 minutes per day each (8 people x 45 min = 6 hours/day)
- AEs receiving a lead see the full conversation context
- The sales manager gained the ability to analyze average call duration and conversion by disposition directly in Kommo reports
Useful side effect: call recordings became accessible in deal cards for onboarding new SDRs - they listen to real examples directly in the CRM.
Who this integration is right for
- SDR teams using the Kixie Power Dialer with Kommo as their CRM
- Companies with an inbound call center on Kixie where contacts are managed in Kommo
- Teams that want AI call transcripts in the CRM card without manual copying
- Situations where SDRs and AEs work in different tools and transparent context handoff is needed
If you are evaluating alternatives for telephony, see comparable integrations: Kommo + Aircall and Kommo + JustCall - they follow the same architecture but have different API specifics.
Frequently asked questions
Does Kixie transcribe all calls or only longer ones?
AI transcription in Kixie (the CI - Conversation Intelligence feature) triggers for calls longer than a certain threshold - typically 30 seconds. Short calls and voicemails are transcribed optionally. The transcript arrives in a separate ci_summary webhook 2-5 minutes after the call, not immediately in call.completed.
How do I avoid duplicate notes if a webhook is delivered twice?
Kixie guarantees at-least-once delivery, meaning duplicates are possible. Use an idempotency key: store the call_id from the payload in Redis with a 24-hour TTL. Before creating a note, check whether this call_id has already been processed.
What if the contact does not exist in Kommo and the SDR is calling a new number?
There are two options. First: create a new contact automatically with the phone number and name from the Kixie payload - suitable for inbound calls. Second: only log unknown numbers to an external log without creating a contact in Kommo - suitable for outbound dialing against a new list that the SDR will qualify first. The choice depends on your process.
Does Kixie support SMS in multiple countries?
Yes. Kixie supports SMS in the US, Canada, UK, and a number of other countries via its own telephony infrastructure. A registered number in Kixie is required for each market. The webhook payload contains country_code, which allows routing events to different Kommo pipelines if you have a multi-regional structure.
Can logging to Kommo be restricted to certain dispositions only?
Yes, and this is recommended practice. In the webhook handler code, add a filter by disposition: write a full note only for connected, a brief entry for voicemail, and no note at all (or only a dial attempt counter increment) for busy and no_answer. This keeps Kommo cards clean.
If your SDRs use Kixie but call history is not reaching Kommo - describe the task to the Exceltic.dev team. This is a typical task: we will discuss call volume, pipeline structure, and propose a solution.