Kommo + Sinch: SMS and Calls from the Pipeline via Cloud Communications Platform

Kommo + Sinch: SMS and Calls from the Pipeline via Cloud Communications Platform

Sinch is a cloud communications platform covering SMS, voice calls, WhatsApp Business, and email. It competes with Twilio and Telnyx and is oriented toward EU and Scandinavian markets - the company is headquartered in Stockholm. Key differentiators: a proprietary SS7 network in Europe, 99.99% SLA, and direct roaming agreements with EU carriers.

The Sinch SMS API uses Basic Auth (Service Plan ID + API Token) or Bearer authentication. Voice calls are handled via the Sinch Voice REST API. For Kommo, the integration enables: sending SMS when a deal changes stage, receiving inbound SMS as a note on the deal card, and initiating an automated TTS call (text-to-speech).

Sinch SMS works through the Messaging API: POST /xms/v1/{service_plan_id}/batches sends an SMS to one recipient or a group. Inbound messages come in via POST /v1/sms/inbounds or a webhook. Delivery reports use a separate webhook endpoint.

Sinch MO (Mobile Originated) - an inbound SMS (from the client to your number). MT (Mobile Terminated) - an outbound SMS (from you to the client). Both directions are handled through a single Service Plan.

Implementation: Outbound SMS on Stage Change

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

app = Flask(__name__)

SINCH_SERVICE_PLAN_ID = os.environ["SINCH_SERVICE_PLAN_ID"]
SINCH_API_TOKEN       = os.environ["SINCH_API_TOKEN"]
SINCH_FROM_NUMBER     = os.environ["SINCH_FROM_NUMBER"]  # +46...
SINCH_BASE            = f"https://us.sms.api.sinch.com/xms/v1/{SINCH_SERVICE_PLAN_ID}"
SINCH_AUTH            = (SINCH_SERVICE_PLAN_ID, SINCH_API_TOKEN)

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

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(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_sms(to: str, body: str) -> str:
    r = requests.post(
        f"{SINCH_BASE}/batches",
        auth=SINCH_AUTH,
        json={
            "from": SINCH_FROM_NUMBER,
            "to":   [to],
            "body": body,
        },
    )
    r.raise_for_status()
    return r.json().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(lead_id)
        if not phone:
            continue

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

        first = name.split()[0] if name else ""
        text  = (
            f"Hello{', ' + first if first else ''}! "
            f"Your inquiry has been reviewed. Our manager will contact you shortly."
        )
        batch_id = send_sms(phone, text)
        add_note(lead_id, f"Sinch SMS sent ({batch_id}) to {phone}.")

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

Implementation: Inbound SMS -> Note in Kommo

@app.route("/webhooks/sinch/inbound", methods=["POST"])
def sinch_inbound():
    event    = request.json or {}
    ev_type  = event.get("type", "")

    if ev_type != "mo_text":  # mo_text = Mobile Originated text SMS
        return jsonify({"status": "ignored"}), 200

    from_num = event.get("from", "")
    body     = event.get("body", "")
    received = event.get("received_at", "")

    lead_id = find_lead_by_phone(from_num)
    if not lead_id:
        return jsonify({"status": "no_lead"}), 200

    add_note(lead_id, f"Sinch inbound SMS from {from_num}: {body}")
    return jsonify({"status": "ok"}), 200

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

@app.route("/webhooks/sinch/delivery", methods=["POST"])
def sinch_delivery():
    # Delivery report for outbound SMS
    event  = request.json or {}
    status = event.get("status", "")  # Delivered, Failed, etc.
    batch_id = event.get("batch_id", "")
    # Log for delivery monitoring
    return jsonify({"status": "ok"}), 200

Voice Calls: TTS (Text-to-Speech)

SINCH_APP_KEY    = os.environ["SINCH_APP_KEY"]
SINCH_APP_SECRET = os.environ["SINCH_APP_SECRET"]
SINCH_VOICE_BASE = "https://calling.api.sinch.com/calling/v1"

def make_tts_call(to: str, message: str) -> str:
    import base64
    auth_str = base64.b64encode(f"{SINCH_APP_KEY}:{SINCH_APP_SECRET}".encode()).decode()
    r = requests.post(
        f"{SINCH_VOICE_BASE}/callouts/tts",
        headers={
            "Authorization": f"Basic {auth_str}",
            "Content-Type":  "application/json",
        },
        json={
            "method":  "ttsCallout",
            "ttsCallout": {
                "cli":      SINCH_FROM_NUMBER,
                "destination": {"type": "number", "endpoint": to},
                "locale":   "en-US",
                "text":     message,
            },
        },
    )
    r.raise_for_status()
    return r.json().get("callId", "")

Regions and Numbers

Sinch provides numbers in 60+ countries. For EU there is a dedicated regional endpoint - eu.sms.api.sinch.com as opposed to us.sms.api.sinch.com. If your clients are primarily in Europe, use the EU endpoint for lower latency and GDPR compliance.

# EU endpoint
SINCH_BASE = f"https://eu.sms.api.sinch.com/xms/v1/{SINCH_SERVICE_PLAN_ID}"

10DLC registration (for the US) and TF (Toll-Free) numbers are managed through the Sinch Dashboard. EU numbers require a separate business verification process.

Who This Is For

B2B companies with EU clients that need a Twilio alternative backed by European infrastructure. Sinch works particularly well for SMS in Scandinavia, Germany, the Netherlands, and the UK. The proprietary SS7 network means fewer intermediate carriers and higher deliverability.

For comparison with alternatives, see: Kommo + Telnyx (low prices, Tier-1 network) and Kommo + Plivo (simple API).

Frequently Asked Questions

How do I get a Sinch SMS Number for sending to Europe?

Sinch Dashboard -> Numbers -> Browse -> select a country -> purchase. For SMS in Germany you need a verified sender ID (alphanumeric, e.g. “MyCompany”). For the UK you need a numeric shared short code or a dedicated long code. The verification process takes 1-3 business days.

Does Sinch support WhatsApp Business API?

Yes - through the Sinch Conversation API, which provides a unified interface for SMS, WhatsApp, RCS, and Viber. Setting up WhatsApp via Sinch takes 1-2 weeks (Meta verification required). For Kommo, integrating WhatsApp through Sinch is an alternative to a direct Meta connection or ready-made Kommo connectors.

How does Sinch ensure GDPR compliance for EU SMS?

Sinch processes data in EU data centers when the EU endpoint is used. A DPA (Data Processing Agreement) is available on request. Message logs are retained for 7 days by default (configurable). Sinch holds ISO 27001 certification and undergoes regular security audits.

Summary

Kommo + Sinch - cloud communications with EU infrastructure:

  • Basic Auth (Service Plan ID + API Token), POST /xms/v1/{service_plan_id}/batches
  • EU endpoint: eu.sms.api.sinch.com for GDPR compliance and lower latency
  • Inbound SMS: webhook mo_text -> find lead by phone -> note
  • TTS call: Sinch Voice API, Basic Auth with App Key + App Secret
  • Delivery reports via a separate webhook endpoint for delivery monitoring

If you need a Kommo integration with Sinch or another cloud communications platform, describe your requirements to the Exceltic.dev team.

More articles

All →