Почему нативная интеграция не работает
OpenPhone - относительно молодая sales phone платформа, которая быстро набирает популярность среди стартапов и SMB в Западной Европе и США. Она предлагает командные номера, запись звонков, совместный inbox, AI-транскрипцию. Для многих компаний OpenPhone заменяет классические системы IP-телефонии вроде Aircall или RingCentral - при меньшей стоимости.
Готовой нативной интеграции OpenPhone + Kommo нет. OpenPhone имеет интеграции с HubSpot и Salesforce через официальные коннекторы, но Kommo в этом списке отсутствует. Без интеграции SDR после каждого звонка вручную открывает Kommo, находит нужную сделку и пишет заметку. При 15-20 звонках в день это 30-45 минут рутины.
Для автоматизации нужно связать OpenPhone Webhooks с Kommo API через кастомный сервис.
Что реализуется - архитектура решения
Основной flow основан на обработке двух типов событий OpenPhone:
OpenPhone: call.completed
--> Webhook --> Python сервис
--> Kommo: поиск контакта по номеру
--> Kommo: создание Note с деталями звонка
--> Kommo: прикрепление ссылки на запись (если есть)
OpenPhone: message.received (входящий SMS)
--> Webhook --> Python сервис
--> Kommo: поиск контакта по номеру
--> Kommo: создание Task для SDR
Технические детали
OpenPhone API Auth. Bearer token. Получается в OpenPhone Settings -> Integrations -> API. Токен не истекает автоматически, но можно ротировать вручную.
OpenPhone Webhooks. Настраиваются в Settings -> Integrations -> Webhooks. Ключевые события:
call.completed- звонок завершён. Payload содержит:from(номер звонящего),to(номер принявшего),direction(inbound/outbound),duration(секунды),recordingUrl(если запись включена),transcript(AI-транскрипция, если включена)message.received- входящий SMS. Payload:from,to,body(текст сообщения)call.ringing- звонок начался (полезно для screen pop в Kommo)
Верификация webhook OpenPhone. OpenPhone подписывает запросы заголовком OpenPhone-Signature. Это HMAC-SHA256 от тела запроса с вашим webhook secret. Аналогично FastSpring.
Матчинг по телефону в Kommo. Kommo API позволяет искать контакты по номеру телефона через GET /api/v4/contacts?query={phone}. Kommo нормализует номера, поэтому +49123456789 и 049123456789 должны находить один контакт. На практике рекомендуется нормализовать номер перед поиском.
Пошаговая реализация
Шаг 1. Нормализация номеров телефона
import re
def normalize_phone(phone: str) -> str:
"""Приводим номер к формату E.164 без + для поиска в Kommo."""
# Убираем все нецифровые символы
digits = re.sub(r"\D", "", phone)
# Убираем ведущий 0 (европейский формат)
if digits.startswith("0") and not digits.startswith("00"):
digits = "0" + digits # оставляем для локального формата
return digits
Шаг 2. Поиск контакта и сделки в 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:
"""Ищем контакт в Kommo по номеру телефона."""
url = f"https://{KOMMO_DOMAIN}/api/v4/contacts"
headers = {"Authorization": f"Bearer {KOMMO_TOKEN}"}
# Пробуем оба формата: с + и без
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:
"""Получаем ID активной сделки для контакта."""
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):
"""Создаём заметку о звонке в сделке 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", # или call_out в зависимости от direction
"params": {
"text": note_text,
"duration": 0, # в секундах, опционально
}
}]
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):
"""Создаём задачу для SDR по входящему SMS."""
url = f"https://{KOMMO_DOMAIN}/api/v4/tasks"
headers = {
"Authorization": f"Bearer {KOMMO_TOKEN}",
"Content-Type": "application/json",
}
import time
# Срок - 4 часа с текущего момента
due_at = int(time.time()) + 4 * 3600
payload = [{
"task_type_id": 1, # 1 = Follow up в Kommo
"text": task_text,
"complete_till": due_at,
"entity_id": lead_id,
"entity_type": "leads",
}]
requests.post(url, json=payload, headers=headers, timeout=10)
Шаг 3. Обработка webhook событий OpenPhone
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):
"""Обрабатываем завершённый звонок."""
direction = data.get("direction", "inbound")
duration_sec = data.get("duration", 0)
recording_url = data.get("recordingUrl", "")
transcript = data.get("transcript", "")
# Определяем номер клиента
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
# Формируем текст заметки
direction_label = "Входящий" if direction == "inbound" else "Исходящий"
duration_min = duration_sec // 60
duration_sec_rem = duration_sec % 60
note_parts = [
f"OpenPhone: {direction_label} звонок",
f"Длительность: {duration_min}:{duration_sec_rem:02d}",
f"Телефон: {client_phone}",
]
if recording_url:
note_parts.append(f"Запись: {recording_url}")
if transcript:
# Обрезаем транскрипт до 500 символов
short_transcript = transcript[:500] + "..." if len(transcript) > 500 else transcript
note_parts.append(f"\nТранскрипт:\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):
"""Обрабатываем входящий 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"Входящий SMS от {from_phone}:\n"
f"{message_body[:200]}"
)
create_sdr_task(lead_id, task_text, contact["id"])
if __name__ == "__main__":
app.run(port=5000)
Реальный кейс с цифрами
В типовом проекте для EU-стартапа с командой из 4 SDR, которые совершают 50-80 звонков в день через OpenPhone, интеграция с Kommo даёт ощутимый результат.
До интеграции: после каждого звонка SDR вручную открывал Kommo, искал сделку, писал заметку. По замерам команды - 2-3 минуты на звонок при хорошем раскладе, 5+ если контакт не находился быстро. При 60 звонках в день - 2-3 часа в день на команду из 4 человек.
После интеграции: заметка появляется автоматически через 5-10 секунд после завершения звонка. Если есть AI-транскрипция OpenPhone - она добавляется в заметку, и менеджер может быстро просмотреть ключевые моменты звонка без прослушивания.
По SMS: раньше SDR проверяли входящие в OpenPhone отдельно от Kommo. После интеграции каждый входящий SMS создаёт задачу «перезвонить» с 4-часовым дедлайном прямо в воронке.
Для кого подходит
Интеграция актуальна для компаний, которые:
- Используют OpenPhone как основную телефонию для sales-команды
- Ведут воронку в Kommo и хотят полную историю коммуникаций в карточке сделки
- Работают в Западной Европе или США, где OpenPhone распространён
- Имеют команду SDR от 2-3 человек, которые совершают регулярные звонки
Если вы уже используете интеграцию Kommo с Aircall или другой телефонией - принцип тот же, но OpenPhone отличается более простым API и доступной ценовой моделью.
Часто задаваемые вопросы
Что делать, если один контакт в Kommo имеет несколько телефонов?
Kommo API возвращает все телефоны контакта в поле custom_fields_values с field_code: PHONE. Для матчинга нормализуйте все номера и проверяйте каждый.
OpenPhone записывает все звонки или нужно включать вручную?
Запись можно включить автоматически для всех звонков в OpenPhone Settings -> Recording. В webhook payload поле recordingUrl будет заполнено только если запись была включена и звонок состоялся (не пропущен).
Что если звонок пропущен - нужно ли создавать заметку?
Рекомендуется. Пропущенный звонок (direction: inbound, duration: 0) стоит фиксировать как задачу с высоким приоритетом для SDR. Это важный сигнал - клиент пытался дозвониться.
Как обрабатывать звонки, если контакта в Kommo нет?
Два варианта: 1) Создавать новый контакт автоматически через POST /api/v4/contacts с номером телефона. 2) Отправлять уведомление в Slack команде с запросом на создание контакта вручную. Второй вариант безопаснее - не засоряет CRM неквалифицированными контактами.
Если вам нужна интеграция Kommo с OpenPhone - опишите ваш стек и сценарий команде Exceltic.dev. Разберём архитектуру за одну встречу.