Kommo + Telnyx: enterprise SMS и звонки из воронки через программируемую телефонию

Telnyx - enterprise-grade CPaaS (Communications Platform as a Service): программируемые SMS, звонки, номера, SIP trunking, AI-транскрипция. Позиционируется как более надёжная и дешёвая альтернатива Twilio с собственной глобальной сетью (не перепродаёт у операторов). Telnyx API использует Bearer token. SMS: POST /v2/messages. Голос: POST /v2/calls. Webhooks: доставляются на WebHook URL или через WebSocket (TeXML - XML-инструкции для управления звонком).

Для Kommo кастомная интеграция с Telnyx позволяет: отправлять SMS при смене этапа, логировать входящие SMS в карточку, инициировать звонок и получать запись разговора.

TeXML - XML-диалект Telnyx для управления звонками: аналог TwiML (Twilio) и PHML (Plivo). Совместим с TwiML на уровне большинства команд.

Сравнение с Twilio и Plivo

TelnyxTwilioPlivo
SMS US$0.004$0.0079$0.0035
Голос US$0.007/мин$0.0135/мин$0.013/мин
ИнфраструктураСобственная сетьПерепродажаПерепродажа
Enterprise SLA99.999%99.95%99.9%
10DLCДаДаДа

Telnyx дороже Plivo по SMS, но дешевле Twilio по голосу и SMS, и имеет собственную глобальную сеть.

Реализация: SMS при смене этапа

import requests, os, hmac, hashlib, base64
from flask import Flask, request, jsonify

app = Flask(__name__)

TELNYX_API_KEY    = os.environ["TELNYX_API_KEY"]  # KEY01234... Bearer token
TELNYX_FROM       = os.environ["TELNYX_PHONE_NUMBER"]  # +1xxxxxxxxxx
TELNYX_PROFILE_ID = os.environ.get("TELNYX_MESSAGING_PROFILE_ID", "")

KOMMO_SUBDOMAIN   = os.environ["KOMMO_SUBDOMAIN"]
KOMMO_TOKEN       = os.environ["KOMMO_ACCESS_TOKEN"]
SMS_STAGE_ID      = int(os.environ["KOMMO_SMS_STAGE_ID"])

TELNYX_BASE = "https://api.telnyx.com/v2"
TELNYX_HDR  = {"Authorization": f"Bearer {TELNYX_API_KEY}", "Content-Type": "application/json"}
KOMMO_BASE  = f"https://{KOMMO_SUBDOMAIN}.kommo.com/api/v4"
KOMMO_HDR   = {"Authorization": f"Bearer {KOMMO_TOKEN}", "Content-Type": "application/json"}

def get_contact_phone_name(lead_id: int) -> tuple[str, str]:
    r = requests.get(
        f"{KOMMO_BASE}/leads/{lead_id}",
        headers=KOMMO_HDR,
        params={"with": "contacts"},
    )
    contacts = r.json().get("_embedded", {}).get("contacts", [])
    if not contacts:
        return "", ""
    rc = requests.get(
        f"{KOMMO_BASE}/contacts/{contacts[0]['id']}",
        headers=KOMMO_HDR,
        params={"with": "custom_fields_values"},
    )
    c = rc.json()
    phone = ""
    for cf in c.get("custom_fields_values", []) or []:
        if cf.get("field_code") == "PHONE":
            vals = cf.get("values", [])
            if vals:
                phone = vals[0].get("value", "")
                break
    return c.get("name", ""), phone

def send_telnyx_sms(to: str, text: str) -> str:
    payload = {
        "from":    TELNYX_FROM,
        "to":      to,
        "text":    text,
        "type":    "SMS",
    }
    if TELNYX_PROFILE_ID:
        payload["messaging_profile_id"] = TELNYX_PROFILE_ID
    r = requests.post(f"{TELNYX_BASE}/messages", headers=TELNYX_HDR, json=payload)
    r.raise_for_status()
    return r.json().get("data", {}).get("id", "")

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 != SMS_STAGE_ID:
            continue

        name, phone = get_contact_phone_name(lead_id)
        if not phone:
            continue

        if not phone.startswith("+"):
            phone = "+" + "".join(c for c in phone if c.isdigit())

        first = name.split()[0] if name else ""
        text  = f"Добрый день{', ' + first if first else ''}! Ваша заявка рассмотрена. Ожидайте звонка менеджера."
        msg_id = send_telnyx_sms(phone, text)
        add_note(lead_id, f"Telnyx SMS отправлен. Message ID: {msg_id}")

    return jsonify({"status": "ok"}), 200

Реализация: входящие SMS и AI-транскрипция звонков

def verify_telnyx_signature(body: bytes, sig_b64: str, ts: str) -> bool:
    # Telnyx: HMAC-SHA256(timestamp + body) с public key (Ed25519)
    # Упрощённая версия: проверяем через webhook signing secret
    secret  = os.environ.get("TELNYX_WEBHOOK_SECRET", "")
    if not secret:
        return True  # если secret не настроен - пропускаем верификацию
    msg     = (ts + body.decode("utf-8")).encode()
    digest  = base64.b64encode(
        hmac.new(secret.encode(), msg, hashlib.sha256).digest()
    ).decode()
    return hmac.compare_digest(digest, sig_b64)

@app.route("/webhooks/telnyx", methods=["POST"])
def telnyx_webhook():
    event      = request.json or {}
    ev_type    = event.get("data", {}).get("event_type", "")
    payload    = event.get("data", {}).get("payload", {})

    if ev_type == "message.received":
        # Входящий SMS
        from_num = payload.get("from", {}).get("phone_number", "")
        text     = payload.get("text", "")

        if from_num and text:
            lead_id = find_lead_by_phone(from_num)
            if lead_id:
                add_note(lead_id, f"Входящий SMS от {from_num}: {text}")

    elif ev_type == "call.transcription":
        # AI-транскрипция завершённого звонка
        transcript  = payload.get("transcription_data", {}).get("transcription", "")
        call_leg_id = payload.get("call_leg_id", "")
        lead_id     = phone_to_lead.get(call_leg_id)
        if lead_id and transcript:
            add_note(int(lead_id), f"Telnyx AI транскрипт:
{transcript[:2000]}")

    elif ev_type == "call.recording.saved":
        # Запись звонка готова
        recording_url = payload.get("recording_urls", {}).get("mp3", "")
        call_leg_id   = payload.get("call_leg_id", "")
        lead_id       = phone_to_lead.get(call_leg_id)
        if lead_id and recording_url:
            add_note(int(lead_id), f"Telnyx запись звонка: {recording_url}")

    return jsonify({"status": "ok"}), 200

phone_to_lead = {}  # {call_leg_id -> lead_id} в production: Redis

def find_lead_by_phone(phone: str) -> int | None:
    r = requests.get(
        f"{KOMMO_BASE}/contacts",
        headers=KOMMO_HDR,
        params={"query": phone, "limit": 5},
    )
    contacts = r.json().get("_embedded", {}).get("contacts", []) or []
    if not contacts:
        return None
    r2 = requests.get(
        f"{KOMMO_BASE}/leads",
        headers=KOMMO_HDR,
        params={"filter[contact_id]": contacts[0]["id"], "limit": 1},
    )
    leads = r2.json().get("_embedded", {}).get("leads", []) or []
    return leads[0]["id"] if leads else None

Telnyx AI Transcription

Telnyx предлагает встроенную AI-транскрипцию через TeXML команду <Record transcribe="true">. После завершения записи Telnyx отправляет webhook call.transcription с полным текстом разговора. Стоимость транскрипции: $0.005/мин (в дополнение к стоимости записи).

<Response>
  <Record transcribe="true"
          transcriptionCallback="https://your-server.com/webhooks/telnyx"
          maxLength="3600"
          playBeep="true" />
</Response>

Для кого актуально

Enterprise B2B компании с высоким объёмом звонков (100+/мес) и требованиями к SLA (Telnyx предоставляет SLA 99.999%). Особенно подходит для компаний, чьи текущие Twilio-расходы превышают $1 000/мес - Telnyx может снизить на 30-50%.

Аналогичные интеграции описаны для Kommo + Twilio и Kommo + Plivo.

Часто задаваемые вопросы

Как Telnyx отличается от Twilio технически?

Telnyx строит на собственной Tier-1 сети - не перепродаёт трафик через AT&T или T-Mobile напрямую, а работает с ними как равный carrier. Это даёт меньшую задержку и меньше промежуточных хопов. Для программируемого API отличий минимум - Telnyx намеренно сделал TeXML совместимым с TwiML для упрощения миграции.

Поддерживает ли Telnyx WhatsApp Business API?

Да, Telnyx предоставляет WhatsApp API через официальное партнёрство с Meta. Настройка аналогична - через Telnyx Portal. Для Kommo интеграция с WhatsApp через Telnyx - альтернатива прямому подключению через Meta.

Как мигрировать с Twilio на Telnyx?

Telnyx поддерживает перенос номеров (number porting) из Twilio. TeXML совместим с TwiML: большинство webhook-handlers работают без изменений. Измените базовый URL (api.telnyx.com вместо api.twilio.com) и переменные Auth. Среднее время миграции для API-интеграций: 1-2 дня.

Итог

Kommo + Telnyx - enterprise CPaaS из воронки:

  • Bearer token TELNYX_API_KEY, SMS через POST /v2/messages
  • Входящие SMS webhook message.received -> find lead by phone -> note
  • AI транскрипция: TeXML <Record transcribe="true"> -> call.transcription webhook
  • Запись звонка: call.recording.saved -> recording_url -> note
  • Дешевле Twilio на 30-50%, SLA 99.999%, собственная глобальная сеть

Если нужна миграция с Twilio на Telnyx или кастомная интеграция с Kommo - опишите задачу команде Exceltic.dev.

Ещё статьи

Все →