|
Professor Seleznov
|
Если вы развиваете маркетплейс или сервис, где клиент общается с выездными специалистами, то важно удерживать звонки внутри платформы. Когда мастер и клиент уходят в прямой контакт, компания теряет комиссию, историю общения и повторные продажи. Полностью избежать обмена номерами невозможно, при встрече исполнитель и клиент могут договориться напрямую. Но если клиент не хочет оставлять личный номер исполнителям и предпочитает гарантии и возможности вашей платформы, такой сценарий помогает сохранять контакт защищённым. В этом материале соберём такой сценарий на Python, используя вместо базы данных Битрикс24. Решение берёт контекст из CRM в момент звонка и через МТС Exolve соединяет клиента, исполнителя или поддержку. Общая схема работы В этом сценарии единый промежуточный номер становится публичной точкой входа для звонка. Исполнитель звонит клиенту через этот номер. Когда клиент перезванивает, приложение смотрит текущую сделку в Битрикс24 и выбирает адресата. Так коммуникация остаётся привязанной к заказу и ответственному исполнителю.
- Исполнитель входит в личный кабинет по коду из СМС. Затем выбирает заказ и нажимает кнопку «Связаться с клиентом». Далее сервис собирает его рабочий контекст из Битрикс24: активные сделки, адрес и номер клиента
- Когда контекст собран, МТС Exolve через Callback API звонит клиенту и исполнителю, и затем соединяет их через промежуточный номер
Стек: Python 3.10+, Streamlit, Flask, Битрикс24 REST API, SMS API и Callback API МТС Exolve. Архитектура решения У сервиса нет своей базы данных и очереди. В Streamlit хранится краткоживущее состояние авторизации, а рабочее состояние заказов остаётся в Битрикс24. За счёт этого приложение связывает CRM и телефонию и не дублирует бизнес-данные. Кабинет исполнителя собран в app.py и authservice.py. App.py отвечает за интерфейс, состояние сессии и действия пользователя. Authservice.py генерирует одноразовый код и отправляет его исполнителю через SMS API. После проверки кода пользователь входит в личный кабинет с заказами. Интеграция с Битрикс24 и МТС Exolve вынесена в bitrix_integration.py, auth_service.py и exolve_voice.py. Первый модуль читает и изменяет данные Битрикс24. Auth_service.py отвечает за аутентификацию исполнителя по СМС. Exolve_voice.py запускает колбэк-звонки через промежуточный номер. Битрикс24 хранит контекст и заказчика, а МТС Exolve даёт каналы с СМС и звонком. Входящие звонки обрабатывает webhook_router.py. Модуль принимает вебхук от телефонии, ищет активный контекст клиента и возвращает JSON с адресатом звонка. В config.py собраны ключи и номера. Пререквизит Проекту нужны только Python-зависимости и переменные окружения для МТС Exolve и Битрикс24. Здесь запускаются два процесса: кабинет исполнителя на Streamlit и сервер для приёма вебхуков на Flask. Первый обслуживает действия пользователя, второй принимает входящие события от телефонии.
python -m venv .venv source .venv/bin/activate pip install -r requirements.txt
Минимальный набор переменных:
EXOLVE_API_KEY=*** AUTH_POOL_NUMBER=7999XXXXXXX SINGLE_SERVICE_NUMBER=7800XXXXXXX SUPPORT_NUMBER=7800YYYYYYY BITRIX_WEBHOOK=https://your-domain.bitrix24.ru/rest/...
Для локального теста достаточно поднять оба процесса и отправить вебхук вручную. Для реального входящего трафика Flask должен быть доступен извне. Шаг 1. Авторизуем исполнителя Исполнитель вводит свой номер телефона в интерфейсе на Streamlit, сервис генерирует одноразовый код и отправляет его в СМС. Пользователь вводит код в форме, а приложение сравнивает его со значением, которое было временно сохранено в сессии.
# auth_service.py import requests from config import Config def send_flash_call(target_phone: str): auth_number = Config.AUTH_POOL_NUMBER url = "https://api.exolve.ru/voice/v1/MakeCall" headers = {"Authorization": f"Bearer {Config.EXOLVE_API_KEY}"} payload = { "number": auth_number, "destination": target_phone, "record": False, "time_limit": 5, } try: resp = requests.post(url, headers=headers, json=payload, timeout=5) resp.raise_for_status() return auth_number[-4:] except Exception as e: print(f"Ошибка Flash Call: {e}") return None
Дальше интерфейс сохраняет сгенерированный код в короткоживущем состоянии и сравнивает его с тем, который пользователь ввёл в форму.
# app.py if not st.session_state.auth: phone = st.text_input("Ваш телефон", placeholder="79990000000") if st.button("Получить код доступа"): with st.spinner("Звоним..."): code = send_flash_call(phone) if code: st.session_state.verification = code st.success("Ждите звонка-сброса. Введите последние 4 цифры.") user_code = st.text_input("Код из звонка") if st.button("Войти"): if user_code == st.session_state.get("verification"): st.session_state.auth = True st.session_state.phone = phone st.rerun()
 После проверки кода кабинет сохраняет номер исполнителя в сессии и дальше использует его как ключ для поиска активных сделок в Битрикс24. Отдельная учётная запись здесь не нужна: один и тот же номер работает и как способ входа, и как идентификатор сотрудника для CRM. Шаг 2. Собираем рабочий контекст исполнителя из Битрикс24 После входа кабинет собирает рабочий контекст на лету из CRM. Сначала ищем в Битрикс24 исполнителя по мобильному номеру. Затем запрашиваем все активные сделки, где этот пользователь назначен ответственным. После этого для каждой сделки добираем контакт клиента и его телефон, чтобы из этих данных собрать карточки в интерфейсе.
# bitrix_integration.py def get_user_id_by_phone(phone: str): method = "user.search" params = {"FILTER": {"PERSONAL_MOBILE": phone}} resp = requests.post(f"{Config.BITRIX_WEBHOOK}/{method}", json=params, timeout=10) resp.raise_for_status() result = resp.json().get("result", []) return result[0]["ID"] if result else None def get_active_deals(master_phone: str): user_id = get_user_id_by_phone(master_phone) if not user_id: return [] params = { "filter": { "ASSIGNED_BY_ID": user_id, "!STAGE_ID": FINAL_STAGES, }, "select": ["ID", "TITLE", "UF_CRM_ADDRESS", "CONTACT_ID"], } resp = requests.post(f"{Config.BITRIX_WEBHOOK}/crm.deal.list", json=params, timeout=10) resp.raise_for_status() deals = [] for item in resp.json().get("result", []): contact_id = item.get("CONTACT_ID") client_phone = _get_contact_phone(contact_id) if contact_id else None if client_phone: deals.append( { "id": item["ID"], "address": item.get("UF_CRM_ADDRESS", "Адрес не указан"), "title": item["TITLE"], "client_phone_hidden": client_phone, } ) return deals
Здесь user_id — это идентификатор исполнителя в Битрикс24, полученный по его номеру телефона. Константа FINAL_STAGES — это список финальных стадий сделки, которые мы исключаем из выборки, чтобы в кабинет попадали только активные заказы. На выходе сервис получает готовые карточки со сделкой, адресом и телефоном клиента. Поле client_phone_hidden хранит исходный номер клиента из CRM, а не промежуточный номер: подмена происходит только в момент звонка через Callback API МТС Exolve. Шаг 3. Звоним через единый номер Когда исполнитель нажимает «Связаться», приложение вызывает метод MakeCallback из API МТС Exolve и передаёт в него три значения: номер сервиса, идентификатор колбэк-ресурса и два плеча вызова: line_1 и line_2. Дальше разговор собирается уже на стороне телефонии.
# exolve_voice.py import requests from config import Config def initiate_masked_call(master_phone: str, client_phone: str): url = "https://api.exolve.ru/voice/v1/Callback" headers = {"Authorization": f"Bearer {Config.EXOLVE_API_KEY}"} payload = { "number": Config.SINGLE_SERVICE_NUMBER, "destination": master_phone, "peer": client_phone, "record": True, } try: requests.post(url, headers=headers, json=payload, timeout=5) except Exception as e: print(f"Callback error: {e}")
Кнопка в кабинете, которая запускает этот вызов.
# app.py if c1.button("📞 Связаться", key=f"call_{deal['id']}"): initiate_masked_call( master_phone=st.session_state.phone, client_phone=deal["client_phone_hidden"], ) st.toast("Соединяем... Ждите входящий.")
 Шаг 4. Принимаем входящий вебхук и ищем активный контекст клиента Обратный звонок клиента запускает второй сценарий: теперь нужно понять, кому именно переводить вызов. При входящем звонке МТС Exolve отправляет на наш сервер JSON-RPC запрос с методом getControlCallFollowMe. Из параметра params.numberA сервис берёт номер вызывающего абонента, ищет по нему контакт в Битрикс24 и проверяет активные сделки.
# webhook_router.py @app.route("/exolve/incoming", methods=["POST"]) def handle_call(): data = request.json or {} caller_phone = data.get("numbers", {}).get("a") master_phone = find_master_phone_by_client(caller_phone)
Находим контакт клиента по номеру телефона.
def find_master_phone_by_client(client_phone: str): params = { "type": "PHONE", "values": [client_phone], "entity_type": "CONTACT", } resp = requests.post( f"{Config.BITRIX_WEBHOOK}/crm.duplicate.findbycomm", json=params, timeout=10, ) resp.raise_for_status() contacts = resp.json().get("result", {}) if not contacts: return None contact_id = contacts[0]
Точка входа здесь одна: номер клиента из numbers.a. По этому номеру сервис поднимает контакт и рабочий контекст клиента в CRM. Шаг 5. Маршрутизируем звонок исполнителю или в поддержку Когда контакт найден, сервис ищет первую активную сделку этого клиента, берёт из неё ответственного и по его идентификатору получает мобильный номер исполнителя. Если такая цепочка собирается целиком, в ответ на вебхук уходит команда перевести звонок этому человеку. Если активной сделки нет или номер ответственного не найден, вызов уходит на номер поддержки.
# webhook_router.py deal_params = { "filter": { "CONTACT_ID": contact_id, "!STAGE_ID": FINAL_STAGES, }, "select": ["ASSIGNED_BY_ID"], } deal_resp = requests.post( f"{Config.BITRIX_WEBHOOK}/crm.deal.list", json=deal_params, timeout=10, ) deal_resp.raise_for_status() deals = deal_resp.json().get("result", []) if not deals: return None assigned_id = deals[0]["ASSIGNED_BY_ID"] user_resp = requests.post( f"{Config.BITRIX_WEBHOOK}/user.get", json={"ID": assigned_id}, timeout=10, )
Находим мобильный номер ответственного.
user_resp.raise_for_status() users = user_resp.json().get("result", []) if users: return users[0].get("PERSONAL_MOBILE") return None
Возвращаем команду на перевод звонка или уводим на резервный маршрут.
redirect_number = master_phone if master_phone else Config.SUPPORT_NUMBER return jsonify({ "id": req_id, "jsonrpc": "2.0", "sip_id": sip_id, "result": { "redirect_type": 1, "followme_struct": [ 1, [ { "I_FOLLOW_ORDER": 1, "ACTIVE": True, "NAME": "Assigned master", "REDIRECT_NUMBER": redirect_number, "PERIOD": "always", "PERIOD_DESCRIPTION": "always", "TIMEOUT": 30 } ] ] } })
На этом этапе Битрикс24 — источник истины для маршрутизации. Не телефония решает, кому звонить, а текущая сделка в CRM. Это удобно как MVP, потому что не нужна своя база соответствий между клиентами и сотрудниками. Шаг 6. Возвращаем изменение статуса обратно в CRM После визита исполнитель может закрыть заказ из того же кабинета. Здесь сервис уже записывает в CRM результат выполнения. Так карточка сделки и реальный статус выезда сохраняются сразу после звонка или визита.
# bitrix_integration.py def closedeal(dealid: str): params = {"id": dealid, "fields": {"STAGEID": "WON"}} requests.post(f"{Config.BITRIX_WEBHOOK}/crm.deal.update", json=params, timeout=10)
Кнопка в кабинете, которая отправляет это обновление.
# app.py if c2.button("✅ Выполнил", key=f"done_{deal['id']}"): close_deal(deal["id"]) st.success("Заказ закрыт!") st.rerun()
Здесь процесс короткий: интерфейс передаёт идентификатор сделки, а Битрикс24 получает новую стадию WON. Запуск и проверка Запускаем сначала сервер для приёма вебхуков, затем интерфейс исполнителя. В отдельных терминалах это выглядит так:
python webhook_router.py streamlit run app.py
Для проверки авторизации в SMS_SENDER укажите зарегистрированный номер отправителя, запросите код в интерфейсе и убедитесь, что СМС дошла на тестовый номер. После этого можно проверить входящий маршрут вручную. Если в SUPPORT_NUMBER задан тестовый номер, а Битрикс24 не найдёт клиентский номер как активный контакт, сервис должен отдать резервный маршрут:
curl -X POST "http://127.0.0.1:5000/exolve/incoming" \ -H "Content-Type: application/json" \ -d '{ "id": 1, "jsonrpc": "2.0", "method": "getControlCallFollowMe", "params": { "sip_id": "7800XXXXXXX", "numberA": "79990000000" } }'
Ожидаемый ответ в минимальном сценарии:
{ "id": 1, "jsonrpc": "2.0", "sip_id": "7800XXXXXXX", "result": { "followme_struct": [ 1, [ { "ACTIVE": true, "I_FOLLOW_ORDER": 1, "NAME": "Assigned master", "PERIOD": "always", "PERIOD_DESCRIPTION": "always", "REDIRECT_NUMBER": "7800YYYYYYY", "TIMEOUT": 30 } ] ], "redirect_type": 1 } }
Дальше проверьте основной сценарий: зайдите в Streamlit под номером исполнителя, убедитесь, что подтянулись активные сделки, нажмите Связаться, а затем вручную отправьте вебхук с номером клиента из CRM. Если получили 4xx или пустой маршрут, сначала посмотрите payload и данные в Битрикс24. Если 5xx, проверьте сетевые таймауты, доступность REST-вебхука и корректность номеров в карточках исполнителей и клиентов. Возможности для развития
- Привести номера телефонов к единому формату на входе и выходе. Без этого поиск по Битрикс24 быстро начинает расходиться на разных форматах записи
- Добавить подпись или иной способ валидации входящего вебхука. Сейчас эндпоинт доверяет любому POST с подходящим JSON
- Вынести коды авторизации из session_state в серверное TTL-хранилище, добавить ограничение частоты отправки СМС и журнал попыток входа
- Добавить реатраи с нарастающей задержкой и сквозной идентификатор события для запросов в МТС Exolve и Битрикс24, чтобы видеть, где именно рвётся цепочка
- Проверять ответы Битрикс24 и МТС Exolve, логировать сбои и показывать их в интерфейсе, чтобы закрытие заказа или запуск звонка не выглядели успешными только на экране
- Определить детерминированные правила маршрутизации для нескольких активных сделок одного клиента вместо правила брать первую
- Подготовить наблюдаемость: структурированные логи, алерты по ошибкам интеграций и простой аудит событий по номеру, сделке и сотруднику
В итоге Получился легковесный сценарий без своей БД. Он работает как связка Битрикс24 и МТС Exolve, где CRM хранит рабочий контекст, а телефония собирает сам разговор. Для MVP это простой способ быстро запустить безопасный сервис с одним промежуточным номером и не раскрывать личные номера исполнителя и клиента, а вход сотрудников организовать через код в СМС без отдельной учётной системы. Дальше такой сценарий можно развивать через голосового робота МТС Exolve: с IVR и генерацией речи можно будет уточнять нужный заказ и точнее маршрутизировать звонки, когда у клиента или исполнителя одновременно открыто несколько заказов, при этом сохраняя один и тот же промежуточный номер.-Источник
|