Kommo + Salesmsg: SMS и звонки из воронки продаж с логом в карточке сделки

Salesmsg - платформа бизнес-SMS и звонков для команд продаж, популярная в США и Канаде: локальные номера, двустороннее SMS, интеграция с CRM. Kommo - CRM с воронкой продаж. Без интеграции SMS-переписка с клиентом существует в Salesmsg, а менеджер переключается между двумя интерфейсами. С интеграцией каждое SMS и звонок из Salesmsg автоматически попадают в заметки карточки сделки в Kommo.

SMS остаётся недооцененным каналом в B2B: open rate около 98% против 20-30% у email. Для north american рынка SMS от локального номера - стандартный способ напомнить о встрече, подтвердить решение, отправить ссылку на документ. Salesmsg предоставляет именно такую инфраструктуру: локальный номер, business messaging compliant, A2P 10DLC регистрация.

Salesmsg - платформа двустороннего бизнес-SMS и звонков с REST API, webhook-уведомлениями о входящих сообщениях и звонках, а также возможностью отправлять SMS программно из любой системы.

Почему это важно для воронки в Kommo

Kommo поддерживает мессенджеры (WhatsApp, Telegram, Instagram), но не SMS на US/CA номера напрямую. Salesmsg закрывает этот пробел для компаний, работающих с north american рынком.

Два направления интеграции:

  1. Kommo -> Salesmsg: при смене статуса сделки автоматически отправить SMS-напоминание клиенту
  2. Salesmsg -> Kommo: входящие SMS и звонки логировать как заметки в карточке сделки

Техническая архитектура

Kommo CRM
  -> deal.status_changed (стадия "Встреча назначена")
  -> POST https://app.salesmsg.com/api/v2/messages/send
     {to: client_phone, body: "Напоминание о встрече..."}

Клиент
  -> Ответить на SMS / позвонить

Salesmsg
  -> POST /your-server/webhooks/salesmsg
     {type: "message.received", contact_phone, body, lead_number}

Ваш сервер
  -> Найти сделку в Kommo по номеру телефона
  -> POST /api/v4/leads/{id}/notes {text: "SMS от клиента: ..."}

Реализация: отправка SMS из Kommo

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

app = Flask(__name__)

KOMMO_DOMAIN    = os.environ["KOMMO_DOMAIN"]
KOMMO_TOKEN     = os.environ["KOMMO_TOKEN"]
SALESMSG_KEY    = os.environ["SALESMSG_API_KEY"]
SALESMSG_SECRET = os.environ["SALESMSG_WEBHOOK_SECRET"]
SMS_FROM_NUMBER = os.environ["SMS_FROM_NUMBER"]   # ваш Salesmsg номер

KOMMO_BASE = f"https://{KOMMO_DOMAIN}/api/v4"
KOMMO_HDR  = {"Authorization": f"Bearer {KOMMO_TOKEN}"}
SM_BASE    = "https://app.salesmsg.com/api/v2"
SM_HDR     = {"Authorization": f"Bearer {SALESMSG_KEY}"}

MEETING_STATUS_ID = 12345   # ID стадии "Встреча назначена"

@app.route("/webhooks/kommo", methods=["POST"])
def kommo_event():
    data = request.json or {}
    for lead in data.get("leads", {}).get("status", []):
        if lead.get("status_id") == MEETING_STATUS_ID:
            send_meeting_reminder(lead["id"])
    return jsonify({"ok": True}), 200

def get_client_phone(lead_id: int) -> str | None:
    r = requests.get(
        f"{KOMMO_BASE}/leads/{lead_id}",
        headers=KOMMO_HDR,
        params={"with": "contacts"},
    )
    if not r.ok:
        return None
    contacts = r.json().get("_embedded", {}).get("contacts", [])
    if not contacts:
        return None

    contact_id = contacts[0]["id"]
    cr = requests.get(f"{KOMMO_BASE}/contacts/{contact_id}", headers=KOMMO_HDR)
    if not cr.ok:
        return None

    for f in cr.json().get("custom_fields_values") or []:
        if f.get("field_code") == "PHONE":
            vals = f.get("values", [])
            if vals:
                return str(vals[0]["value"])
    return None

def send_meeting_reminder(lead_id: int):
    phone = get_client_phone(lead_id)
    if not phone:
        return

    body = (
        "Добрый день! Напоминаем о нашей встрече. "
        "Если возникли вопросы - ответьте на это сообщение."
    )

    r = requests.post(
        f"{SM_BASE}/messages/send",
        headers=SM_HDR,
        json={
            "to":   phone,
            "from": SMS_FROM_NUMBER,
            "body": body,
        },
        timeout=10,
    )
    if r.ok:
        requests.post(
            f"{KOMMO_BASE}/leads/{lead_id}/notes",
            headers=KOMMO_HDR,
            json=[{"note_type": "common", "params": {"text": f"SMS отправлено: {body}"}}],
        )

Реализация: логирование входящих сообщений в Kommo

import hmac, hashlib

def verify_salesmsg_webhook(raw_body: bytes, sig_header: str) -> bool:
    expected = hmac.new(
        SALESMSG_SECRET.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, sig_header)

@app.route("/webhooks/salesmsg", methods=["POST"])
def salesmsg_event():
    sig = request.headers.get("X-Salesmsg-Signature", "")
    if not verify_salesmsg_webhook(request.data, sig):
        return jsonify({"error": "unauthorized"}), 401

    data       = request.json or {}
    event_type = data.get("type", "")
    from_phone = data.get("contact_phone", "")
    body_text  = data.get("body", "")

    if event_type == "message.received" and from_phone:
        log_sms_to_kommo(from_phone, body_text, direction="in")

    elif event_type == "call.completed":
        duration  = data.get("duration", 0)
        recording = data.get("recording_url", "")
        log_call_to_kommo(from_phone, duration, recording)

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

def find_lead_by_phone(phone: str) -> int | None:
    # Search Kommo contacts by phone number.
    r = requests.get(
        f"{KOMMO_BASE}/contacts",
        headers=KOMMO_HDR,
        params={"query": phone, "limit": 1},
    )
    if not r.ok:
        return None
    contacts = r.json().get("_embedded", {}).get("contacts", [])
    if not contacts:
        return None
    # Найти связанные сделки
    contact_id = contacts[0]["id"]
    lr = requests.get(
        f"{KOMMO_BASE}/contacts/{contact_id}/links",
        headers=KOMMO_HDR,
    )
    if not lr.ok:
        return None
    leads = [l for l in lr.json().get("_embedded", {}).get("links", []) if l.get("to_entity_type") == "leads"]
    return leads[0]["to_entity_id"] if leads else None

def log_sms_to_kommo(phone: str, text: str, direction: str):
    lead_id = find_lead_by_phone(phone)
    if not lead_id:
        return
    direction_label = "Входящее SMS" if direction == "in" else "Исходящее SMS"
    requests.post(
        f"{KOMMO_BASE}/leads/{lead_id}/notes",
        headers=KOMMO_HDR,
        json=[{"note_type": "common", "params": {"text": f"{direction_label} ({phone}): {text}"}}],
    )

def log_call_to_kommo(phone: str, duration: int, recording_url: str):
    lead_id = find_lead_by_phone(phone)
    if not lead_id:
        return
    note_text = f"Звонок Salesmsg ({phone}): {duration}с."
    if recording_url:
        note_text += f" Запись: {recording_url}"
    requests.post(
        f"{KOMMO_BASE}/leads/{lead_id}/notes",
        headers=KOMMO_HDR,
        json=[{"note_type": "call_in", "params": {"text": note_text, "duration": duration}}],
    )

Сценарии использования

Автоматические SMS по стадиям воронки:

  • “Встреча назначена” -> напоминание за день до встречи
  • “КП отправлено” -> “Получили наше предложение? Готовы ответить на вопросы.”
  • “Счёт выставлен” -> напоминание об оплате через 3 дня

Входящие SMS в Kommo:

  • Клиент отвечает на SMS -> заметка в карточке сделки с текстом
  • Менеджер видит всю SMS-переписку в хронологии сделки

Звонки:

  • call.completed webhook -> заметка с длительностью и ссылкой на запись

Реальный кейс

SaaS-компания с US-клиентами, 6 менеджеров. До интеграции: SMS вёлась в личных телефонах менеджеров, нет лога в CRM, при уходе менеджера теряется история. После интеграции: все SMS и звонки через корпоративный Salesmsg номер, каждое сообщение в карточке сделки. Onboarding нового менеджера с историей переписки занял 15 минут вместо нескольких часов выяснения контекста.

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

Компании с US/CA клиентами, использующие SMS как основной канал follow-up. Если ваши менеджеры активно пишут клиентам с личных телефонов - это первый сигнал: коммуникация существует вне CRM и при уходе сотрудника теряется.

Смежная задача - телефонные звонки с транскриптами - разобрана в статье о интеграции IP-телефонии с Kommo.

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

Как найти сделку в Kommo если клиент пишет с нового номера?

Если номер не совпадает ни с одним контактом в Kommo - создайте новый контакт и новую сделку через API. Укажите тег “Salesmsg inbound” чтобы менеджеры могли отфильтровать такие лиды.

Поддерживает ли Salesmsg MMS и файлы?

Да, Salesmsg поддерживает MMS (фото, PDF). В webhook-событии поле media_urls содержит массив URL вложений. Добавьте ссылки в заметку Kommo вместе с текстом.

Как обеспечить соответствие требованиям A2P 10DLC?

A2P 10DLC (Application-to-Person 10-Digit Long Code) - регуляторное требование для бизнес-SMS в США. Salesmsg проводит вас через процесс регистрации Brand и Campaign. Без регистрации carrier carriers фильтруют сообщения. Среднее время одобрения - 3-5 рабочих дней.

Итог

Kommo + Salesmsg связывает SMS/звонки и CRM:

  • Kommo webhook -> автоматические SMS по событиям воронки
  • Входящие SMS -> заметки в карточке сделки через find-by-phone
  • Завершённые звонки -> note с длительностью и записью
  • Webhook верификация через HMAC-SHA256 заголовок

Если ваша команда работает с US/CA рынком и хочет перевести SMS-коммуникацию в CRM-контекст - обратитесь в Exceltic.dev. Настроим интеграцию под ваш стек воронки.

Ещё статьи

Все →