Kommo + Plivo: SMS and Calls from Your Sales Pipeline via Programmable Telephony

Plivo is a cloud communications platform (CPaaS): programmable SMS, voice calls, and WhatsApp via API. It operates in 190+ countries and is popular as a more affordable alternative to Twilio - SMS and call rates are 30-50% lower with comparable coverage. For Kommo, a custom integration enables: sending SMS when a deal stage changes, logging inbound SMS into the deal card, and initiating outbound calls directly from Kommo.

Plivo uses a REST API with Basic Auth (Auth ID + Auth Token). Outbound SMS: POST /v1/Account/{auth_id}/Message/. Outbound calls: POST /v1/Account/{auth_id}/Call/ with an Answer URL that returns XML (Plivo Markup Language, PHML) containing call instructions.

Plivo Markup Language (PHML) is an XML format for controlling call behavior: play a message, connect to an agent, record the conversation. It is the equivalent of TwiML in Twilio.

Key Integration Scenarios

Scenario 1: When a deal moves to the “Send Proposal” stage - automatically send the client an SMS with a link to the proposal.

Scenario 2: Client sends an inbound SMS -> Plivo fires a webhook -> create a note in Kommo.

Scenario 3: A manager clicks “Call” in Kommo -> Plivo initiates the call through the browser or mobile device.

Implementation: Outbound SMS on Stage Change

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

app = Flask(__name__)

PLIVO_AUTH_ID    = os.environ["PLIVO_AUTH_ID"]
PLIVO_AUTH_TOKEN = os.environ["PLIVO_AUTH_TOKEN"]
PLIVO_FROM_NUMBER = os.environ["PLIVO_FROM_NUMBER"]  # e.g. +12025551234

KOMMO_SUBDOMAIN  = os.environ["KOMMO_SUBDOMAIN"]
KOMMO_TOKEN      = os.environ["KOMMO_ACCESS_TOKEN"]
KP_STAGE_ID      = int(os.environ["KOMMO_KP_STAGE_ID"])  # "Send Proposal" stage

PLIVO_BASE = f"https://api.plivo.com/v1/Account/{PLIVO_AUTH_ID}"
PLIVO_AUTH = (PLIVO_AUTH_ID, PLIVO_AUTH_TOKEN)

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 "", ""
    contact_id = contacts[0]["id"]
    rc = requests.get(
        f"{KOMMO_BASE}/contacts/{contact_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, text: str) -> str:
    # to: number in E.164 format: +12025551234
    r = requests.post(
        f"{PLIVO_BASE}/Message/",
        auth=PLIVO_AUTH,
        json={
            "src":  PLIVO_FROM_NUMBER,
            "dst":  to,
            "text": text,
            "type": "sms",
        },
    )
    r.raise_for_status()
    return r.json().get("message_uuid", [None])[0]

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

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

        # Normalize phone number (simplified)
        digits_only = "".join(c for c in phone if c.isdigit())
        if not phone.startswith("+"):
            phone = "+" + digits_only

        sms_text = (
            f"Hi {name.split()[0] if name else ''}! "
            f"We're sending over our proposal. Reply to this message or give us a call."
        )
        msg_uuid = send_sms(phone, sms_text)
        add_note(lead_id, f"Plivo SMS sent to {phone}. UUID: {msg_uuid}")

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

Implementation: Inbound SMS -> Kommo

def verify_plivo_signature(auth_id: str, auth_token: str, uri: str, params: dict, signature: str) -> bool:
    # Plivo signs: auth_id + nonce (from X-Plivo-Nonce header) + URI
    # Details: https://www.plivo.com/docs/sms/concepts/inbound-sms-webhooks/
    # Simplified check via X-Plivo-Signature
    sorted_params = "".join(f"{k}{v}" for k, v in sorted(params.items()))
    msg    = uri + sorted_params
    digest = hmac.new(auth_token.encode(), msg.encode(), hashlib.sha1).digest()
    import base64
    computed = base64.b64encode(digest).decode()
    return hmac.compare_digest(computed, signature)

@app.route("/webhooks/plivo/inbound-sms", methods=["POST"])
def plivo_inbound_sms():
    params = request.form.to_dict()
    from_number = params.get("From", "")
    text        = params.get("Text", "")

    if not from_number or not text:
        return "<?xml version="1.0" encoding="utf-8" ?><Response></Response>", 200

    # Find the deal by phone number in Kommo
    lead_id = find_lead_by_phone(from_number)
    if lead_id:
        add_note(lead_id, f"Inbound SMS from {from_number}: {text}")

    # Plivo expects an XML response (PHML)
    return "<?xml version="1.0" encoding="utf-8" ?><Response></Response>", 200

def find_lead_by_phone(phone: str) -> int | None:
    # Search by phone number via Kommo Contacts API
    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
    contact_id = contacts[0]["id"]
    # Find an open deal for this contact
    r2 = requests.get(
        f"{KOMMO_BASE}/leads",
        headers=KOMMO_HDR,
        params={"filter[contact_id]": contact_id, "filter[status_id]": "active", "limit": 1},
    )
    leads = r2.json().get("_embedded", {}).get("leads", []) or []
    return leads[0]["id"] if leads else None

Implementation: Outbound Calls via Plivo

When a button is clicked in Kommo (via a custom action or manually triggered webhook), Plivo initiates a call: it first rings the agent, then connects them to the client.

ANSWER_URL = os.environ["PLIVO_ANSWER_URL"]  # URL that serves the PHML

@app.route("/call/initiate", methods=["POST"])
def initiate_call():
    data         = request.json or {}
    agent_phone  = data.get("agent_phone")   # agent's phone number
    client_phone = data.get("client_phone")   # client's phone number
    lead_id      = data.get("lead_id")

    r = requests.post(
        f"{PLIVO_BASE}/Call/",
        auth=PLIVO_AUTH,
        json={
            "from":       PLIVO_FROM_NUMBER,
            "to":         agent_phone,  # ring the agent first
            "answer_url": f"{ANSWER_URL}/phml/connect?client={client_phone}&lead={lead_id}",
            "answer_method": "GET",
        },
    )
    r.raise_for_status()
    return jsonify({"call_uuid": r.json().get("request_uuid")}), 200

@app.route("/phml/connect", methods=["GET"])
def phml_connect():
    client_phone = request.args.get("client", "")
    lead_id      = request.args.get("lead", "")
    # PHML: once the agent answers, dial the client
    phml = (
        '<?xml version="1.0" encoding="utf-8" ?>
'
        '<Response>
'
        '  <Speak>Connecting you to the client.</Speak>
'
        f'  <Dial callerId="{PLIVO_FROM_NUMBER}" record="true" recordingCallbackUrl="{ANSWER_URL}/webhooks/plivo/recording">
'
        f'    <Number>{client_phone}</Number>
'
        '  </Dial>
'
        '</Response>'
    )
    return phml, 200, {"Content-Type": "application/xml"}

@app.route("/webhooks/plivo/recording", methods=["POST"])
def plivo_recording():
    params = request.form.to_dict()
    recording_url = params.get("RecordUrl", "")
    lead_id       = params.get("lead", "")  # passed via callback URL parameter
    if recording_url and lead_id:
        add_note(int(lead_id), f"Plivo: call recording: {recording_url}")
    return "", 200

Plivo Pricing

As of Q2 2026:

  • SMS to the US: $0.0035/message (outbound)
  • SMS to the UK: $0.04/message
  • Calls to the US: $0.013/min (outbound)
  • Phone number: $0.80/month

For comparison: Twilio - SMS $0.0079, calls $0.015/min.

Real-World Case

A real estate agency with 12 managers and 80 leads per month. Plivo was used to send property viewing reminders. Before the integration: SMS were sent manually. After: when a deal moves to the “Viewing Scheduled” stage, an SMS with the property address and appointment time is sent automatically. Time saved: 2 hours/week on routine messaging.

Who This Is For

Companies with high SMS volumes (50+ per month) in countries where Twilio rates are steep. Particularly relevant for agencies, service companies, and B2B SaaS with SMS onboarding flows. Plivo is most effective when you need to keep communication CAC low.

Similar integrations are covered for Kommo + Twilio and Kommo + Salesmsg.

Frequently Asked Questions

Is 10DLC registration required for SMS in the US via Plivo?

Yes. For A2P (Application-to-Person) SMS in the US, brand and campaign registration with The Campaign Registry (TCR) is required - the same applies to Twilio and other providers. Plivo provides registration tools through their Dashboard. Unregistered campaigns are blocked by carriers.

Does Plivo support WhatsApp?

Yes, via the Plivo WhatsApp API (in beta as of Q2 2026). It works through the official WhatsApp Business API and supports template and session messages. Production use requires Meta approval.

How do you handle delivery reports (SMS delivery status)?

Plivo sends DLR (Delivery Status Callback) to the URL specified in the url parameter when sending an SMS. Statuses: sent, delivered, failed. Add this URL in your API call and log the status as a note in Kommo.

Summary

Kommo + Plivo - programmable communications from your sales pipeline:

  • Basic Auth (Auth ID + Auth Token), E.164 for phone numbers
  • Kommo webhook -> Plivo SMS API -> log UUID as a note
  • Inbound SMS: Plivo webhook -> find deal by phone -> add note
  • Outbound calls: POST /Call/ -> PHML connect -> recording
  • 30-50% cheaper than Twilio, suitable for high volumes

If you need a Kommo integration with Plivo or another CPaaS provider - describe your requirements to the Exceltic.dev team.

More articles

All →