Kommo + Telnyx: Enterprise SMS and Calls from the Pipeline via Programmable Telephony

Kommo + Telnyx: Enterprise SMS and Calls from the Pipeline via Programmable Telephony

Telnyx is an enterprise-grade CPaaS (Communications Platform as a Service): programmable SMS, voice calls, phone numbers, SIP trunking, and AI transcription. It is positioned as a more reliable and cost-effective alternative to Twilio, built on its own global network (rather than reselling from carriers). The Telnyx API uses Bearer token authentication. SMS: POST /v2/messages. Voice: POST /v2/calls. Webhooks: delivered to a WebHook URL or via WebSocket (TeXML - XML instructions for call control).

With a custom Kommo integration, Telnyx enables: sending SMS on stage change, logging inbound SMS to the deal card, initiating calls, and receiving call recordings.

TeXML is Telnyx’s XML dialect for call control - analogous to TwiML (Twilio) and PHML (Plivo). It is compatible with TwiML at the level of most commands.

Comparison with Twilio and Plivo

TelnyxTwilioPlivo
SMS US$0.004$0.0079$0.0035
Voice US$0.007/min$0.0135/min$0.013/min
InfrastructureOwn networkResellerReseller
Enterprise SLA99.999%99.95%99.9%
10DLCYesYesYes

Telnyx is more expensive than Plivo for SMS, but cheaper than Twilio for both voice and SMS, and operates its own global network.

Implementation: SMS on Stage Change

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"Hello{', ' + first if first else ''}! Your request has been reviewed. Our manager will call you shortly."
        msg_id = send_telnyx_sms(phone, text)
        add_note(lead_id, f"Telnyx SMS sent. Message ID: {msg_id}")

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

Implementation: Inbound SMS and AI Call Transcription

def verify_telnyx_signature(body: bytes, sig_b64: str, ts: str) -> bool:
    # Telnyx: HMAC-SHA256(timestamp + body) with public key (Ed25519)
    # Simplified version: verify via webhook signing secret
    secret  = os.environ.get("TELNYX_WEBHOOK_SECRET", "")
    if not secret:
        return True  # skip verification if secret is not configured
    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":
        # Inbound 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"Inbound SMS from {from_num}: {text}")

    elif ev_type == "call.transcription":
        # AI transcription of completed call
        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:\n{transcript[:2000]}")

    elif ev_type == "call.recording.saved":
        # Call recording is ready
        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 call recording: {recording_url}")

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

phone_to_lead = {}  # {call_leg_id -> lead_id} in 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 offers built-in AI transcription via the TeXML command <Record transcribe="true">. After the recording ends, Telnyx sends a call.transcription webhook with the full call transcript. Transcription cost: $0.005/min (in addition to the recording cost).

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

Who This Is For

Enterprise B2B companies with high call volumes (100+/month) and SLA requirements (Telnyx offers a 99.999% SLA). Particularly well-suited for companies whose current Twilio spend exceeds $1,000/month - Telnyx can reduce costs by 30-50%.

Similar integrations are described for Kommo + Twilio and Kommo + Plivo.

Frequently Asked Questions

How does Telnyx differ from Twilio technically?

Telnyx builds on its own Tier-1 network - rather than reselling traffic through AT&T or T-Mobile, it peers with them as an equal carrier. This results in lower latency and fewer intermediate hops. From a programmable API perspective the differences are minimal - Telnyx intentionally made TeXML compatible with TwiML to simplify migration.

Does Telnyx support WhatsApp Business API?

Yes, Telnyx provides WhatsApp API access through an official Meta partnership. Setup is similar - via the Telnyx Portal. For Kommo, integrating WhatsApp through Telnyx is an alternative to a direct connection through Meta.

How do I migrate from Twilio to Telnyx?

Telnyx supports number porting from Twilio. TeXML is compatible with TwiML: most webhook handlers work without changes. Update the base URL (api.telnyx.com instead of api.twilio.com) and your auth variables. Average migration time for API integrations: 1-2 days.

Summary

Kommo + Telnyx - enterprise CPaaS from within the pipeline:

  • Bearer token TELNYX_API_KEY, SMS via POST /v2/messages
  • Inbound SMS webhook message.received -> find lead by phone -> note
  • AI transcription: TeXML <Record transcribe="true"> -> call.transcription webhook
  • Call recording: call.recording.saved -> recording_url -> note
  • 30-50% cheaper than Twilio, 99.999% SLA, own global network

If you need to migrate from Twilio to Telnyx or build a custom Kommo integration - describe your requirements to the Exceltic.dev team.

More articles

All →