Защищаем личные номера телефонов на маркетплейсах: соединяем клиента и исполнителя

Страницы:  1

Ответить
 

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()
pic
После проверки кода кабинет сохраняет номер исполнителя в сессии и дальше использует его как ключ для поиска активных сделок в Битрикс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("Соединяем... Ждите входящий.")
pic
Шаг 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 и генерацией речи можно будет уточнять нужный заказ и точнее маршрутизировать звонки, когда у клиента или исполнителя одновременно открыто несколько заказов, при этом сохраняя один и тот же промежуточный номер.-Источник
 
Loading...
Error