Привет, Хабр!
Разберём, как собрать сервис для автоматической обработки лидов. Для сделок купли‑продажи он будет моментально запускать обратный звонок и соединять клиента с риелтором. В сценариях аренды сервис будет запускать голосового робота: тот соберёт вводные данные, отправит агенту карточку лида и после подтверждения времени показа направит клиенту СМС.
Стек: Python, Flask, SQLAlchemy, SQLite, Callback и SMS API.
Общая схема работы
CRM или другой внешний источник данных отправляет данные о лиде: номер телефона, тип сделки и идентификатор объекта. Эти данные приводятся к международному формату, проверяется наличие объекта в базе данных и создаётся запись о сделке. Если идентификатор объекта в базе отсутствует, заявка отправляется на ручную проверку.
Для покупки и продажи сервис запускает обратный звонок через Callback API МТС Exolve. Платформа сначала дозванивается риелтору, а затем соединяет его с клиентом. В аренде сначала голосовой робот уточняет у потенциального арендатора параметры запроса и возвращает ответы через вебхук.
Все статусы звонков, ответы клиента и действия риелтора сохраняются в SQLite. Это позволяет отслеживать состояние сделки на каждом этапе и автоматически уведомлять риелтора или отправку СМС с подтверждением встречи клиенту.
Клиент │ оставляет заявку на объект ▼ Flask-сервис │ POST /api/v1/lead ├─ нормализует номер ├─ проверяет объект ├─ создаёт сделку в SQLite └─ выбирает сценарий по типу сделки │ ├─ покупка / продажа │ ▼ │ Callback API МТС Exolve │ └─ соединяет риелтора с клиентом │ └─ аренда ▼ Голосовой робот МТС Exolve └─ собирает вводные арендатора ▼ Flask-сервис ├─ сохраняет ответы ├─ отправляет карточку лида риелтору └─ после подтверждения риелтора отправляет клиенту СМС
Лид поступает на POST /api/v1/lead
Сервис нормализует номер, проверяет объект и создаёт сделку в SQLite
Сервис запускает колбэк или Голосового робота
МТС Exolve отправляет вебхуки со статусами и результатами сценария
Состояние сделки обновляется и риелтор получает уведомления
Клиент получает СМС с деталями встречи
Архитектура решения
Всё решение состоит из пяти частей: HTTP‑слоя, сервиса сценариев, клиента API Платформы МТС Exolve, SQLite‑хранилища и модуля уведомлений.
HTTP‑слой принимает входящие события: заявки из внешних систем и вебхуки от МТС Exolve. Он проверяет данные, приводит их к внутреннему формату сервиса и передаёт в сервис сценариев.
Сервис сценариев хранит основную бизнес‑логику сделки. Он проверяет наличие объекта недвижимости в базе, назначает риелтора и выбирает способ связи. В результате модуль запускает колбэк, передаёт заявку голосовому роботу или отправляет лид на ручную проверку.
Звонки, голосовой робот и СМС вынесены в отдельный модуль интеграции с МТС Exolve. Благодаря этому основной код сценария остаётся про сделку: кого соединить, когда запустить робота и какой статус сохранить.
SQLite хранит данные об объектах недвижимости и состояние каждой сделки. В базе фиксируются технические идентификаторы звонков и ответы клиентов из вебхуков. Это позволяет сервису восстановить контекст при получении события от платформы и определить следующий шаг сценария.
Модуль уведомлений обрабатывает коммуникации после изменения статуса сделки. Он формирует карточку лида для риелтора и отправляет её через Unisender или SMTP. После подтверждения времени показа сервис отправляет клиенту СМС.
Внешний источник заявок │ │ JSON: телефон, тип сделки, object_id ▼ app.py ├─ маршруты и HTTP-контракты ├─ проверка заявки и выбор сценария ├─ обработка вебхуков Exolve └─ действия риелтора по одноразовым ссылкам │ │ │ ▼ ▼ ▼ database.py services/exolve_client.py services/email_client.py │ │ │ ▼ ▼ ▼ SQLite API МТС Exolve Unisender или SMTP сделки, объекты, Callback, робот, карточка лида риелторы, токены СМС для риелтора
Как устроены данные сделки
После создания заявки сценарий становится асинхронным: статусы звонков приходят вебхуками от МТС Exolve, результат запуска голосового робота — отдельным вебхуком, а риелтор подтверждает действие по ссылке из письма. Поэтому в базе хранится не только лид, но и рабочее состояние сделки.
Основная запись — сделка. Она объединяет телефон клиента, объект недвижимости, тип операции, назначенного риелтора, статусы обратного звонка, голосового робота и СМС, ответы клиента после опроса и выбранное время показа.
Сессия вызова связывает сделку с идентификатором звонка МТС Exolve. Это позволяет вебхукам корректно попадать в контекст сделки и обновлять нужный статус. Токены действий обеспечивают работу одноразовых ссылок из писем риелтору, а слоты просмотров фиксируют забронированное время встречи.
Сделка ├─ Лид: телефон, объект, тип операции ├─ Риелтор и выбранный объект ├─ Статусы: Callback, Голосовой робот, СМС ├─ Ответы клиента после опроса └─ Время показа Сессия вызова └─ Связь сделки с идентификаторами МТС Exolve Токен действия └─ Одноразовые ссылки для риелтора Слот показа └─ Забронированное время встречи
Пререквизит
Для работы понадобится Python 3.10 или выше. Необходимо загрузить зависимости из файла requirements.txt.
python -m venv .venv source .venv/bin/activate pip install -r requirements.txt
Для интеграции с МТС Exolve потребуются API‑ключ, номер для исходящих вызовов, идентификаторы ресурсов колбэка и кампания Голосового робота. BASE_URL должен содержать публичный HTTPS‑адрес сервера. Платформа отправляет на него вебхуки, а приложение использует этот адрес для формирования ссылок риелтору.
Создайте файл.env:
SECRET_KEY=change_me DEBUG=false PORT=5000 BASE_URL=https://example.com DATABASE_URL=sqlite:///real_estate.db DISPLAY_TIMEZONE=Europe/Moscow EXOLVE_API_KEY=... EXOLVE_NUMBER=7800XXXXXXX EXOLVE_CALLBACK_RESOURCE_ID=12345 EXOLVE_CAMPAIGN_ID=... EXOLVE_CAMPAIGN_URL=https://api.exolve.ru/campaign/v1/Call EMAIL_FROM=info@example.com UNISENDER_API_KEY= UNISENDER_LIST_ID=1 SMTP_HOST=smtp.yandex.ru SMTP_PORT=465 SMTP_USER= SMTP_PASSWORD=
Приложение проверяет конфигурацию на старте. Если не заполнить обязательные параметры или оставить в них тестовые значения, сервис прервёт работу до обработки первой заявки. Необходимо заменить все значения своими: плейсхолдеры вроде example.com и номер с иксами не пройдут проверку.
Если не указать ключ API Unisender, приложение переключится на SMTP. При отсутствии данных для авторизации почтового сервера включится тестовый режим, и система будет записывать текст писем в лог.
Шаг 1. Принимаем лид и создаём ключ идемпотентности
Первым делом сервис проверяет структуру заявки, нормализует телефон, сохраняет источник обращения и готовит данные для маршрутизации. Некорректные запросы отклоняются сразу. Валидные заявки, которые нельзя обработать автоматически, переводятся в ручную проверку, а по остальным создаются сделки в базе и передаются дальше.
@app.route("/api/v1/lead", methods=["POST"]) def receive_lead(): data = get_json_object() if data is None: return jsonify({"error": "JSON body must be an object"}), 400 phone = normalize_phone(data.get("phone")) object_id = normalize_object_id(data.get("object_id")) deal_type = str(data.get("deal_type") or "").strip().lower() source = str(data.get("source") or "").strip() or None if not phone or deal_type not in {"renter", "landlord", "sell", "buy"}: return jsonify({"error": "Invalid params"}), 400
Бэкенд приводит номер телефона к формату из 11 цифр с префиксом 7 и проверяет тип сделки. Доступны четыре категории сделок: аренда, сдача, продажа и покупка. Если структура JSON в запросе нарушена или параметры не прошли валидацию, сервис возвращает ошибку 400 и прерывает регистрацию лида.
Чтобы исключить дублирование данных, создаётся ключ идемпотентности на основе номера телефона, объекта недвижимости и типа сделки. Если в течение 15 минут поступает повторный запрос с тем же ключом, новая запись в базе не создаётся. Это предотвращает появление лишних сделок при случайном многократном нажатии кнопки в интерфейсе.
idempotency_key = hashlib.md5( f"{phone}:{object_id or 'none'}:{deal_type}".encode() ).hexdigest() existing_deal = db.query(Deal).filter( Deal.idempotency_key == idempotency_key, Deal.created_at >= utc_now() - timedelta(minutes=15) ).first() if existing_deal: return jsonify({"status": "duplicate"}), 200
У схемы с хешем есть ограничение. Если сделка создалась, а следующий шаг сценария не выполнился из‑за сбоя, повторная заявка с теми же полями в течение 15 минут вернёт статус duplicate без перезапуска обработки.
В текущей схеме ключ формируется из бизнес‑полей лида. Если с заявкой передаётся собственный уникальный идентификатор заявки, то вместо генерации хеша используется он. Это даёт более строгую проверку при интеграции с API Платформой МТС Exolve.
Внешний идентификатор устойчивее. По нему видно, что пришла та же самая заявка, и может проверить состояние сделки, а при неуспешной первой попытке — запустить сценарий заново.
Шаг 2. Направляем сделку по типу операции
Сервис распределяет лиды по сценариям в зависимости от типа сделки. Для купли‑продажи запускается колбэк через API Платформу МТС Exolve и сохраняется идентификатор звонка. Это нужно для отслеживания статуса соединения и контроля работы риелтора.
if deal_type in ["buy", "sell"]: if not realtor: new_deal.status = "manual_check" db.commit() return jsonify({"status": "manual_check", "reason": "realtor_not_found"}), 200 request_description = f"deal_{new_deal.id}_main" call_id = exolve.initiate_callback( operator_phone=realtor.phone, client_phone=phone, request_description=request_description, callback_resource_id=Config.EXOLVE_CALLBACK_RESOURCE_ID, ) new_deal.exolve_call_id = str(call_id) new_deal.callback_status = "requested_main"
Бэкенд проверяет, назначен ли объекту риелтор. Если данных нет, сделка получает статус ручной проверки. Если ответственный найден — запускается обратный звонок.
В сделках по аренде голосовой робот проводит анкетирование. Чтобы связать ответы клиента с записью в базе, идентификаторы передаются в метаданные запроса в поле initialData при старте кампании.
def start_robo_call(self, client_phone: str, deal_id: int, object_id: int) -> str: payload = { "campaign_id": Config.EXOLVE_CAMPAIGN_ID, "params": { "destination": client_phone, "initialData": { "deal_id": str(deal_id), "object_id": str(object_id), "scenario": "renter_real_estate" } } } response = requests.post(Config.EXOLVE_CAMPAIGN_URL, json=payload, headers=self.headers, timeout=20) response.raise_for_status()
Метод start_robo_call формирует запрос для API Платформы. Метаданные в initialData помогают сопоставить входящий вебхук со сделкой и обновить состояние записи в базе данных.
Внешние вызовы работают синхронно. При ошибке API Платформы сделка переводится в режим ручной проверки и возвращается ошибка 500.
Шаг 3. Обрабатываем события колбэка
API Платформа МТС Exolve отправляет уведомления о статусе вызова через вебхуки. Далее анализируется тип события и сторона соединения: риелтор или клиент.
Бэкенд находит сделку в базе по идентификаторам из запроса. Если уведомление пришло от устаревшей сессии — например, после переключения на другого сотрудника — такие данные игнорируются. Это не позволяет старому звонку перезаписать актуальный статус сделки.
Обработчик обновляет состояние сделки на каждом этапе. Когда риелтор принимает вызов, сервис фиксирует ответ первой стороны. После успешного соединения с клиентом сделка получает финальный статус.
if event_side == "A" and event_type == "s": deal.callback_operator_answered = True deal.callback_status = "operator_answered" db.commit() return jsonify({"status": "operator_answered"}), 200 if event_side == "B" and event_type == "s": deal.callback_client_answered = True deal.callback_status = "connected" deal.status = "completed" db.commit() return jsonify({"status": "connected"}), 200
Технические события телефонии переводятся в бизнес‑логику. Если основной риелтор не отвечает, система ищет в базе резервного сотрудника и запускает новый колбэк. Если резервных контактов нет или вызов снова пропущен, заявка переходит в режим ручной проверки.
Шаг 4. Получаем результат голосового робота
Для сделок по аренде данные принимаются на эндпоинт POST /webhook/voice/robot‑results. МТС Exolve передаёт статус вызова, идентификаторы сделки и звонка, а также собранные ответы клиента.
Бэкенд находит сделку в базе данных по её идентификатору. Если его нет в запросе, ищется запись по call_id и связанная запись через CallSession или exolve_call_id.
После успешного опроса ответы сохраняются и обновляется статус на robot_completed. Находится ответственный риелтор, формируются временные слоты для показа и генерируются одноразовые ссылки для управления сделкой.
if data.get("status") == "success": c_data = data.get("collected_data") or {} deal.who_will_live = c_data.get("who_will_live") deal.has_pets = c_data.get("has_pets") deal.ready_to_move = c_data.get("ready_to_move") deal.convenient_view_time = c_data.get("convenient_view_time") deal.robot_status = "completed" deal.status = "robot_completed" db.commit()
Обработчик разбирает анкету и обновляет состояние сделки. Так результат опроса сохраняется до отправки карточки риелтору.
После успешной отправки письма сделка переводится в статус realtor_notified. Если почтовый провайдер вернул ошибку, ссылки аннулируются и сделку переводится на ручную проверку.
Шаг 5. Отправляем карточку лида риелтору
Риелтор управляет заявкой через письмо по электронной почте. Оно содержит адрес объекта, цену, контакты клиента и результаты опроса, который провёл голосовой робот. В тело сообщения встроены ссылки для действий: подтверждение времени показа, запуск обратного вызова или уточнение данных.
Для каждой ссылки генерируется уникальный одноразовый токен, который действует 24 часа. Ключ аннулируется после первого использования. Если риелтор выбирает один вариант, сервис автоматически инвалидирует остальные ссылки по этой сделке.
Бэкенд проверяет наличие только токена в базе данных без валидации цифровой подписи ссылки.
Шаг 6. Регистрируем показ и отправляем СМС
При выборе времени риелтором проверяется его расписание. Если в интервале часа до и после выбранного слота нет других встреч, бронируется время и обновляется статус сделки. Это предотвращает накладки в графике сотрудников.
if token_record.action in ["confirm_slot_1", "confirm_slot_2"]: target_slot = ensure_aware_utc(token_record.slot_time) if not check_realtor_availability(db, deal.realtor_id, target_slot): return render_template_string(REALTOR_RESPONSE_HTML, message="Слот занят."), 200 db.add(ViewingSlot(deal_id=deal.id, realtor_id=realtor.id, selected_slot=target_slot)) deal.selected_slot = target_slot deal.status = "confirmed" deal.confirmed_realtor_id = realtor.id invalidate_deal_tokens(db, deal.id) try: db.commit() except IntegrityError: db.rollback() return render_template_string(REALTOR_RESPONSE_HTML, message="Слот занят."), 200
В базе данных настроен уникальный индекс для пары из идентификатора риелтора и времени слота. При одновременных запросах на одно и то же время SQLite вернёт ошибку целостности — IntegrityError. Проверка на уровне СУБД защищает систему от создания дублирующих бронирований.
Ошибку перехватывает обработчик подтверждения: сервис откатывает транзакцию и показывает риелтору сообщение о занятом слоте. Так проверка в приложении и ограничение в СУБД дают одинаковый результат для пользователя.
После сохранения записи сервис отправляет СМС через SMS API МТС Exolve. Если API возвращает ошибку, бронирование остаётся в силе: сделка сохраняет статус confirmed, а sms_status получает значение failed. Система не откатывает подтверждённый слот из‑за сбоя уведомления.
Запуск и проверка
При первом старте создаются таблицы в SQLite, добавляются тестовые риелторы и демонстрационный объект.
python app.py
Проверьте эндпоинт через curl. При запросе на аренду сервис создаст запись в базе и запустит голосового робота.
curl -X POST http://localhost:5000/api/v1/lead \ -H "Content-Type: application/json" \ -d '{ "phone": "+7 999 555-44-33", "object_id": 1, "deal_type": "renter", "source": "site" }'
После интеграции с МТС Exolve возвращается «status»: “robot_calling”. В сценарии купли‑продажи при deal_type: “buy” бэкенд отправит callback_initiated. Повторный запрос в течение 15 минут вернёт статус duplicate. Так работает механизм защиты от дублирования лидов.
Укажите публичный BASE_URL для вебхуков в личном кабинете API Платформы. Для тестов на локальной машине используйте туннелирование. API Платформа должна иметь доступ к адресам /webhook/voice/call‑event и /webhook/voice/robot‑results. Это синхронизирует состояние сделки с событиями телефонии и ответами Голосового робота.
Результат
Созданный сервис автоматизирует первичный контакт и убирает задержку между заявкой и первым действием. Потенциальный клиент сразу соединяется с риелтором, а голосовой робот задаёт базовые вопросы или переводит сложный случай в ручную обработку. Для бизнеса снижается риск потери лида.
Для развития проекта можно: подключить CRM, добавить аналитику по потерянным лидам и расширить сценарии под другие направления, где важно быстро обработать входящую заявку. Этот же подход можно адаптировать для автодилеров, медицинских клиник, сервисных компаний и других бизнесов с высокой ценой пропущенного обращения.
Код на GitHub.

