Привет, Хабр. Это Екатерина Саяпина, менеджер продукта в МТС Exolve. Сегодня покажу, как реализовать анонимный обратный звонок с сайта через Callback API — ни клиент, ни менеджер не видят номера друг друга, соединение идёт через виртуальный номер. Всё на Django, просто и надёжно.
Сайт с кнопкой обратной связи
Для примера используем простую витрину предложений по аренде — соберём её на Django. Он хорошо подходит для прототипов и продакшен-решений, особенно когда важны читаемость, шаблоны и быстрая работа с БД.
Предполагаем, что у вас уже есть Django-проект и приложение callback_app.
Теперь добавим витрину с формой, страницу ожидания и обработчик соединения.
Для этого настроим маршрутизацию в файле urls.py в папке вашего приложения:
urlpatterns = [
path('admin/', admin.site.urls),
path('', views.show_page),
]
По домашнему адресу вызывается функция из файла views.py, которая возвращает клиенту нашу витрину:
def show_page(request):
return render(request, 'callback_app/index.html')
На витрине, в углу страницы, размещена кнопка заказа обратного звонка.

При нажатии на неё всплывает форма обратной связи.

Отправка формы запроса звонка
Простая форма отправляет имя и номер телефона через POST-запрос. На тесте можно работать с localhost, но в проде обязательно используйте переменные окружения для конфигурации адреса.
contactForm.addEventListener('submit', async (e) => {
e.preventDefault();
// Получаем данные формы
const name = document.getElementById('name').value;
const phone = document.getElementById('phone').value;
// Показываем индикатор загрузки
btnText.innerHTML = '<span class="loading"></span> Отправка...';
submitBtn.disabled = true;
// Формируем JSON объект
const formData = {
name: name,
phone: phone
};
try {
// Отправляем POST запрос
const response = await fetch('http://localhost:8000/get_data/', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
// Обрабатываем ответ
if (response.ok) {
showResponse('Данные успешно отправлены!', 'success');
contactForm.reset();
} else {
showResponse('Ошибка при отправке данных', 'error');
}
} catch (error) {
showResponse('Ошибка сети: ' + error.message, 'error');
} finally {
// Восстанавливаем кнопку
btnText.textContent = 'Отправить';
submitBtn.disabled = false;
// Автоматически скрываем сообщение через 5 секунд
setTimeout(() => {
responseMessage.style.display = 'none';
}, 5000);
}
});
Полный код скрипта опубликован на GitHub, ссылка в конце статьи.
Создаём ресурсы на API Платформе
Вся интеграция с телеком-платформой реализуется через utils.py. API-ключ, номера, параметры аудио вынесены в .env. Такой подход безопаснее, удобно переключать окружения.
import json
import requests
import os
from .types_def import MakeCallBackParams, Audio, Settings
exolve_api_key = os.environ['EXOLVE_API_KEY']
manager_phone = os.environ['MANAGER_PHONE']
application_phone = os.environ['APPLICATION_PHONE']
hello_message_params = {
"full_name": "Hello_rent",
"description": "",
"text": "Здравствуйте. Один момент, специалист подключается",
"voice_settings": {
}
}
callback_resource_params = {
"callback_name": "Rent_site",
"description": "Соединение клиента сайта-витрины и специалиста"
}
Конфиденциальные данные — API-ключ, номера телефонов и параметры соединения — вынесены в переменные окружения. Ключ доступа можно получить в личном кабинете после регистрации и создания приложения. Внутри него — все необходимые сущности: ключи, CallBack-ресурсы, привязанные номера и TTS-записи. Без аренды номеров функции звонков и SMS недоступны — для нашего решения потребуется минимум два виртуальных номера.
MANAGER_PHONE — номер менеджера для соединения, APPLICATION_PHONE — арендованный номер платформы, через который инициируются вызовы. Оба номера должны быть в том же приложении, что и используемый API-ключ. Параметры ресурсов оформлены в словари-константы, структура запросов описана через dataclasses в types_def.py.
Создание услуги обратного звонка
Для соединения двух номеров необходимо создать CallBack-ресурс — это цифровая сущность, которая получает уникальный идентификатор и используется в последующих запросах.
Создание выполняется через метод Create API Платформы. Вызов оформлен в функции create_callback_resource:
def create_callback_resource():
r = requests.post(r'https://api.exolve.ru/callback/v1/Create', headers={'Authorization': 'Bearer ' + exolve_api_key},
data=json.dumps(callback_resource_params))
if r.status_code == 200:
out_data = json.loads(r.text)
return int(out_data['callback_resource_id'])
В ответ приходит идентификатор созданного ресурса — он сохраняется и используется при инициировании вызова. Создавать CallBack-ресурс нужно один раз на приложение, повторно — только при необходимости переопределения параметров.
Создание аудио с заготовкой
Чтобы воспроизвести клиенту сообщение во время дозвона, создаём аудиозапись на основе текста с помощью TTS через метод SynthesizeAndSave. Генерация выполняется один раз, результат сохраняется и переиспользуется по resource_id.
Платформа также поддерживает онлайн-синтез TTS на лету, без предварительной генерации. Но каждый такой вызов платный, поэтому в большинстве случаев выгоднее использовать заранее сохранённую запись.
Функция create_record отправляет запрос на генерацию аудио и возвращает идентификатор созданной записи:
def create_record():
r = requests.post(r'https://api.exolve.ru/media/v1/SynthesizeAndSave', headers={'Authorization': 'Bearer ' + exolve_api_key},
data=json.dumps(hello_message_params))
if r.status_code == 200:
out_data = json.loads(r.text)
return int(out_data['resource_id'])
По аналогии создаётся CallBack-ресурс, через который инициируются вызовы. Оба объекта — аудиозапись и CallBack — проверяются при запуске приложения. Если они отсутствуют, создаются новые, и результат записывается в файл настроек:
def check_resources():
if not os.path.exists(sett_path):
audio = Audio(create_record())
sets = Settings(callback_resource_id=create_callback_resource(), audio=audio)
with open(sett_path, 'wt') as f:
f.write(sets.to_json())
with open(sett_path, 'rt') as f:
sets_str = f.read()
return Settings.schema().loads(sets_str)
Настройки хранятся локально, в JSON-формате. При последующих запусках ресурсы повторно не создаются, если файл присутствует и валиден. Формат и сериализация описаны далее в types_def.py.
Объект с настройками загружается с диска при старте приложения, а при необходимости может быть обновлён через HTTP-запрос. Если в нём нет идентификаторов нужных ресурсов, они создаются автоматически, а полученные значения сохраняются на диск. Это позволяет избежать повторной генерации при каждом запуске и поддерживать устойчивую работу без дополнительной логики на стороне клиента.
В следующем разделе — структура используемых dataclass-объектов и описание параметров запросов к API.
Объекты с настройками
Датаклассы определяются в файле types_def. Нам понадобится три библиотеки: так проще работать с JSON.
from dataclasses import dataclass
from dataclasses_json import dataclass_json
from dataclass_wizard import JSONWizard
С библиотекой dataclasses многие уже знакомы, а dataclasses_json упрощает преобразования датакласса в JSON-строку. Декоратор dataclass_json упрощает как экспорт датакласса в JSON, так и его чтение из строки.
От класса JSONWizad можно наследовать свои датаклассы, чтобы получить дополнительные возможности. Здесь мы его используем, чтобы не включать в экспортируемую JSON-строку те поля, которые не заполнены, то есть имеют значение, установленное по умолчанию.
В частности, аудиозапись мы проигрываем только клиенту, поэтому это поле останется пустым. Таким же образом можно не определять любые необязательные поля.
На жёстком диске сохраняется JSON, соответствующий классу Settings. Он содержит номера создаваемых ресурсов, которые используются для соединения.
@dataclass
class Audio:
media_resource_id: int
@dataclass_json
@dataclass
class Settings:
callback_resource_id: int
audio: Audio
Класс Audio хранит идентификатор ресурса CallBack и используется также для звонка.
@dataclass
class Destination:
number: str
@dataclass
class LineX(JSONWizard):
class _(JSONWizard.Meta):
skip_defaults = True
destinations: list[Destination] | None = None
display_number: str = ''
audio: Audio | None = None
def set_number(self, number: str):
self.destinations = [Destination(number)]
def add_number(self, number: str):
if type(self.destinations) is not list:
self.destinations = []
self.destinations.append(Destination(number))
@dataclass
class MakeCallBackParams(JSONWizard):
class _(JSONWizard.Meta):
skip_defaults = True
number_code: int # номер для создания обратного звонка
callback_resource_id: int # уникальный идентификатор ресурса обратного звонка
line_1: LineX | None = None
line_2: LineX | None = None
def add_client_data(self, number: str, audio: Audio, display_num: str = ''):
self.line_1 = LineX(display_number=display_num, audio=audio)
self.line_1.add_number(number)
def add_manager_data(self, number: str, display_num: str = ''):
self.line_2 = LineX(display_number=display_num)
self.line_2.add_number(number)
Датакласс MakeCallBackParams отражает структуру запроса к методу API MakeCallBack. Поле callback_resource_id соответствует аналогичному полю класса Settings и содержит номер ресурса CallBack. Поле number_code должно содержать один из номеров, привязанных к вашему приложению.
Поля line_1 и line_2 содержат объекты с параметрами обоих плеч созвона — от платформы клиенту и от платформы менеджеру. Все параметры описаны на странице метода MakeCallBack. Если с обратными звонками работает несколько менеджеров, может быть соответствующее количество номеров — для поочерёдного обзвона. Поэтому поле destinations содержит список объектов класса Destination.
Звоним по API
При нажатии на кнопку отправки формы обратного звонка открывается гиперссылка, которая ведёт на страницу «Спасибо за обращение», параллельно отправляется JSON, содержащий введённые данные.
Для обработки запроса добавляем в views.py следующую функцию:
@csrf_exempt
def get_data(request):
form_data = json.loads(request.body.decode('utf-8'))
setts = check_resources()
callback_params = create_request(setts, form_data)
make_callback(callback_params.to_dict())
return render(request, 'deep_seek_api_app/index.html')
Функция загружает объект настроек, создаёт запрос к API и возвращает ту же страницу. Определим в файле urls.py соответствие между адресами запроса и функциями его обработки:
urlpatterns = [
path('admin/', admin.site.urls),
path('', views.show_page),
path('get_data/', views.get_data),
]
Результат
После отправки формы пользователю поступает входящий вызов — обычно в течение нескольких секунд. Менеджер компании подключается автоматически, с другой линии. Оба абонента видят только номер, указанный в параметре application_phone, — личные данные остаются скрытыми.
Ответ от API возвращается сразу после приёма запроса, без ожидания завершения звонка. Поэтому на веб-странице можно быстро отобразить подтверждение, не блокируя интерфейса.
Во время ожидания пользователь прослушает заранее сгенерированное сообщение. Это снизит риск того, что он сбросит звонок до соединения. Сценарий прозрачно работает для всех сторон: каждый получает вызов с платформы и не видит номера другой стороны.
Выводы
Мы собрали минимальное рабочее решение обратного звонка через API Платформу МТС Exolve. Вся логика укладывается в один API-запрос, с раздельной настройкой для каждой стороны, поддержкой TTS и сохранением анонимности участников.
Такой вариант подходит для проектов, где важно быстро соединить клиента со специалистом: например, для маркетплейсов, интернет-магазинов, сервисов аренды недвижимости или авто, медицинских услуг, технической поддержки и обслуживания.
Данные пользователей не сохраняются, номера не раскрываются — платформа полностью берёт на себя маршрутизацию вызовов. При необходимости можно расширить решение: добавить CRM, аналитику, авторизацию или очередь вызовов.
Полный исходный код — на GitHub.