Why the native integration doesn’t work
OpenPhone is a relatively young sales phone platform gaining rapid popularity among startups and SMBs in Western Europe and the US. It offers shared numbers, call recording, a collaborative inbox, and AI transcription. For many companies OpenPhone replaces classic VoIP systems like Aircall or RingCentral - at a lower price point.
There is no ready-made native OpenPhone + Kommo integration. OpenPhone has integrations with HubSpot and Salesforce via official connectors, but Kommo is not on that list. Without integration, an SDR manually opens Kommo after each call, finds the right deal, and writes a note. At 15-20 calls per day that’s 30-45 minutes of routine.
Automation requires connecting OpenPhone Webhooks with the Kommo API via a custom service.
What gets built - solution architecture
The primary flow is based on handling two types of OpenPhone events:
OpenPhone: call.completed
--> Webhook --> Python service
--> Kommo: find contact by phone number
--> Kommo: create Note with call details
--> Kommo: attach recording link (if available)
OpenPhone: message.received (inbound SMS)
--> Webhook --> Python service
--> Kommo: find contact by phone number
--> Kommo: create Task for SDR
Technical details
OpenPhone API Auth. Bearer token. Obtained in OpenPhone Settings -> Integrations -> API. The token does not expire automatically but can be rotated manually.
OpenPhone Webhooks. Configured in Settings -> Integrations -> Webhooks. Key events:
call.completed- call ended. Payload contains:from(caller’s number),to(receiver’s number),direction(inbound/outbound),duration(seconds),recordingUrl(if recording is enabled),transcript(AI transcription, if enabled)message.received- inbound SMS. Payload:from,to,body(message text)call.ringing- call started (useful for screen pop in Kommo)
OpenPhone webhook verification. OpenPhone signs requests with the OpenPhone-Signature header. This is HMAC-SHA256 of the request body using your webhook secret. Same approach as FastSpring.
Phone matching in Kommo. The Kommo API allows searching contacts by phone number via GET /api/v4/contacts?query={phone}. Kommo normalizes numbers, so +49123456789 and 049123456789 should find the same contact. In practice, normalizing the number before searching is recommended.
Step-by-step implementation
Step 1. Phone number normalization
import re
def normalize_phone(phone: str) -> str:
"""Normalize number to E.164 format without + for Kommo search."""
# Remove all non-digit characters
digits = re.sub(r"\D", "", phone)
# Remove leading 0 (European format)
if digits.startswith("0") and not digits.startswith("00"):
digits = "0" + digits # keep for local format
return digits
Step 2. Find contact and deal in Kommo
import os
import requests
KOMMO_DOMAIN = os.environ["KOMMO_DOMAIN"]
KOMMO_TOKEN = os.environ["KOMMO_ACCESS_TOKEN"]
def find_contact_by_phone(phone: str) -> dict | None:
"""Search for a contact in Kommo by phone number."""
url = f"https://{KOMMO_DOMAIN}/api/v4/contacts"
headers = {"Authorization": f"Bearer {KOMMO_TOKEN}"}
# Try both formats: with + and without
for query in [phone, normalize_phone(phone)]:
r = requests.get(url, params={"query": query}, headers=headers, timeout=10)
if r.ok:
contacts = r.json().get("_embedded", {}).get("contacts", [])
if contacts:
return contacts[0]
return None
def get_active_lead_for_contact(contact_id: int) -> int | None:
"""Get the active deal ID for a contact."""
url = f"https://{KOMMO_DOMAIN}/api/v4/contacts/{contact_id}/links"
headers = {"Authorization": f"Bearer {KOMMO_TOKEN}"}
r = requests.get(url, headers=headers, timeout=10)
if not r.ok:
return None
links = r.json().get("_embedded", {}).get("links", [])
lead_ids = [l["to_entity_id"] for l in links if l.get("to_entity_type") == "leads"]
return lead_ids[0] if lead_ids else None
def create_call_note(lead_id: int, note_text: str):
"""Create a call note on a deal in Kommo."""
url = f"https://{KOMMO_DOMAIN}/api/v4/leads/{lead_id}/notes"
headers = {
"Authorization": f"Bearer {KOMMO_TOKEN}",
"Content-Type": "application/json",
}
payload = [{
"note_type": "call_in", # or call_out depending on direction
"params": {
"text": note_text,
"duration": 0, # in seconds, optional
}
}]
r = requests.post(url, json=payload, headers=headers, timeout=10)
return r.ok
def create_sdr_task(lead_id: int, task_text: str, contact_id: int | None = None):
"""Create a task for an SDR on an inbound SMS."""
url = f"https://{KOMMO_DOMAIN}/api/v4/tasks"
headers = {
"Authorization": f"Bearer {KOMMO_TOKEN}",
"Content-Type": "application/json",
}
import time
# Due date - 4 hours from now
due_at = int(time.time()) + 4 * 3600
payload = [{
"task_type_id": 1, # 1 = Follow up in Kommo
"text": task_text,
"complete_till": due_at,
"entity_id": lead_id,
"entity_type": "leads",
}]
requests.post(url, json=payload, headers=headers, timeout=10)
Step 3. Handle OpenPhone webhook events
import hmac
import hashlib
from flask import Flask, request, abort
app = Flask(__name__)
OPENPHONE_WEBHOOK_SECRET = os.environ["OPENPHONE_WEBHOOK_SECRET"]
def verify_openphone_signature(payload: bytes, signature: str) -> bool:
expected = hmac.new(
OPENPHONE_WEBHOOK_SECRET.encode("utf-8"),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
@app.route("/webhooks/openphone", methods=["POST"])
def openphone_webhook():
signature = request.headers.get("OpenPhone-Signature", "")
if not verify_openphone_signature(request.get_data(), signature):
abort(403)
event = request.json
event_type = event.get("type")
data = event.get("data", {}).get("object", {})
if event_type == "call.completed":
handle_call_completed(data)
elif event_type == "message.received":
handle_message_received(data)
return {"ok": True}
def handle_call_completed(data: dict):
"""Handle a completed call."""
direction = data.get("direction", "inbound")
duration_sec = data.get("duration", 0)
recording_url = data.get("recordingUrl", "")
transcript = data.get("transcript", "")
# Determine client's number
if direction == "inbound":
client_phone = data.get("from", "")
note_type = "call_in"
else:
client_phone = data.get("to", "")
note_type = "call_out"
contact = find_contact_by_phone(client_phone)
if not contact:
print(f"Contact not found for phone: {client_phone}")
return
lead_id = get_active_lead_for_contact(contact["id"])
if not lead_id:
return
# Build note text
direction_label = "Inbound" if direction == "inbound" else "Outbound"
duration_min = duration_sec // 60
duration_sec_rem = duration_sec % 60
note_parts = [
f"OpenPhone: {direction_label} call",
f"Duration: {duration_min}:{duration_sec_rem:02d}",
f"Phone: {client_phone}",
]
if recording_url:
note_parts.append(f"Recording: {recording_url}")
if transcript:
# Trim transcript to 500 characters
short_transcript = transcript[:500] + "..." if len(transcript) > 500 else transcript
note_parts.append(f"\nTranscript:\n{short_transcript}")
note_text = "\n".join(note_parts)
url = f"https://{KOMMO_DOMAIN}/api/v4/leads/{lead_id}/notes"
headers = {
"Authorization": f"Bearer {KOMMO_TOKEN}",
"Content-Type": "application/json",
}
payload = [{"note_type": note_type, "params": {
"text": note_text,
"duration": duration_sec,
"phone": client_phone,
}}]
requests.post(url, json=payload, headers=headers, timeout=10)
def handle_message_received(data: dict):
"""Handle an inbound SMS."""
from_phone = data.get("from", "")
message_body = data.get("body", "")
contact = find_contact_by_phone(from_phone)
if not contact:
return
lead_id = get_active_lead_for_contact(contact["id"])
if not lead_id:
return
task_text = (
f"Inbound SMS from {from_phone}:\n"
f"{message_body[:200]}"
)
create_sdr_task(lead_id, task_text, contact["id"])
if __name__ == "__main__":
app.run(port=5000)
Real case with numbers
In a typical project for an EU startup with a team of 4 SDRs making 50-80 calls per day via OpenPhone, integration with Kommo delivers a tangible result.
Before the integration: after each call an SDR manually opened Kommo, found the deal, wrote a note. By the team’s own measurement - 2-3 minutes per call in the best case, 5+ if the contact wasn’t found quickly. At 60 calls per day - 2-3 hours per day for a team of 4.
After the integration: the note appears automatically 5-10 seconds after the call ends. If OpenPhone AI transcription is available, it’s added to the note, and the manager can quickly review the key call moments without listening to the recording.
For SMS: previously SDRs checked inbound messages in OpenPhone separately from Kommo. After the integration, each inbound SMS creates a “call back” task with a 4-hour deadline directly in the pipeline.
Who this is for
The integration is relevant for companies that:
- Use OpenPhone as the primary phone system for the sales team
- Track the pipeline in Kommo and want full communication history in the deal card
- Operate in Western Europe or the US where OpenPhone is common
- Have an SDR team of 2-3+ people who make regular calls
If you already use Kommo with Aircall or another phone system - the principle is the same, but OpenPhone has a simpler API and a more accessible pricing model.
Frequently asked questions
What if one contact in Kommo has multiple phone numbers?
The Kommo API returns all contact phone numbers in the custom_fields_values field with field_code: PHONE. For matching, normalize all numbers and check each one.
Does OpenPhone record all calls or does it need to be enabled manually?
Recording can be enabled automatically for all calls in OpenPhone Settings -> Recording. The recordingUrl field in the webhook payload will only be populated if recording was enabled and the call was answered (not missed).
If a call is missed - should a note still be created?
Recommended. A missed call (direction: inbound, duration: 0) should be recorded as a high-priority task for the SDR. It’s an important signal - the client was trying to reach someone.
How to handle calls when the contact doesn’t exist in Kommo?
Two options: 1) Automatically create a new contact via POST /api/v4/contacts with the phone number. 2) Send a Slack notification to the team requesting manual contact creation. The second option is safer - it doesn’t pollute the CRM with unqualified contacts.
If you need a Kommo + OpenPhone integration - describe your stack and scenario to the Exceltic.dev team. We’ll work through the architecture in one meeting.