Одна из самых эффективных техник конверсии — мгновенный звонок клиенту после заполнения формы. Но настоящая магия происходит, когда система соединяет клиента напрямую с менеджером без ожидания в очереди. Разбираем архитектуру и код.
Как это работает: архитектура
Клиент (браузер)
│
▼ 1. Заполняет форму: имя + телефон
│
Бэкенд-сервер
│
▼ 2. Генерирует подпись + отправляет API-запрос
│
SIP-провайдер (АТС)
│
├─► 3a. Звонок на номер клиента
│
└─► 3b. Звонок на номер/внутр. номер менеджера
│
▼ 4. Автоматическое соединение каналов при ответе
│
Двусторонний разговор (клиент ↔ менеджер)
│
▼ 5. Вебхук: уведомление о статусе/длительности
│
Логирование в БД / CRM
Ключевой момент: АТС не «ждёт ответа» от клиента, а одновременно набирает оба номера и соединяет каналы при ответе любого участника. Это называется Click-to-Call или Callback в терминах телефонии.
Минимальная реализация на Node.js
1. Фронтенд: форма с валидацией
<form id="callbackForm">
<input type="text" id="name" placeholder="Имя" required>
<input type="tel" id="phone" placeholder="+7 (999) 123-45-67" required>
<button type="submit">Перезвонить сейчас</button>
<div id="status"></div>
</form>
<script>
document.getElementById('callbackForm').addEventListener('submit', async (e) => {
e.preventDefault();
const phone = document.getElementById('phone').value.replace(/\D/g, '');
if (phone.length < 11) {
alert('Неверный формат телефона');
return;
}
const res = await fetch('/api/callback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
phone: phone.replace(/^8/, '7'), // 8999 → 7999
name: document.getElementById('name').value
})
});
const data = await res.json();
document.getElementById('status').textContent =
data.success ? '✓ Звонок инициирован!' : 'Ошибка: ' + data.error;
});
</script>
2. Бэкенд: обработчик + подпись для API
// server.js
const express = require('express');
const axios = require('axios');
const crypto = require('crypto');
const app = express();
app.use(express.json());
// Конфигурация (из .env)
const CONFIG = {
PBX_ID: process.env.PBX_ID,
API_KEY: process.env.API_KEY,
API_SALT: process.env.API_SALT,
MANAGER_EXT: '101' // внутренний номер менеджера
};
// Генерация подписи для провайдеров с MD5-аутентификацией
function generateSign(params) {
const sorted = Object.keys(params).sort()
.map(k => `${k}=${params[k]}`)
.join('&');
return crypto.createHash('md5').update(sorted + CONFIG.API_SALT).digest('hex');
}
// Инициация двустороннего звонка
app.post('/api/callback', async (req, res) => {
try {
const { phone, name } = req.body;
// Форматирование номера под требования провайдера
const external = phone.startsWith('7') ? phone : '7' + phone.slice(1);
// Параметры для АТС
const params = {
pbx_id: CONFIG.PBX_ID,
api_key: CONFIG.API_KEY,
internal: CONFIG.MANAGER_EXT, // Куда звонить менеджеру
external: external, // Куда звонить клиенту
caller_id: external // Отображаемый номер
};
// Подпись запроса
params.sign = generateSign(params);
// Запрос к АТС (универсальный пример)
const response = await axios.post(
'https://api.provider.ru/v1/call', // URL зависит от провайдера
new URLSearchParams(params),
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
);
// Успешный ответ = звонок поставлен в очередь АТС
if (response.data.code === 200 || response.data.success) {
console.log(`📞 Звонок запущен: ${external} ↔ ${CONFIG.MANAGER_EXT}`);
// Опционально: сохраняем в БД для аналитики
// await db.calls.insert({ phone, name, timestamp: new Date() });
res.json({ success: true, message: 'Звонок инициирован' });
} else {
throw new Error(response.data.message || 'Ошибка АТС');
}
} catch (err) {
console.error('❌ Ошибка звонка:', err.message);
res.status(500).json({ success: false, error: 'Не удалось инициировать звонок' });
}
});
app.listen(3000, () => console.log('Сервер запущен на порту 3000'));
Важные нюансы реализации
Формат номеров
Провайдеры требуют строгий формат:
79991234567— без+, с кодом страны- Номера 8-800 часто требуют отдельной настройки
- Международные звонки — префикс страны + локальный номер
Поддержка вебхуков
Для отслеживания статуса звонка настройте эндпоинт:
app.post('/webhook/call-status', express.raw({ type: 'application/json' }), (req, res) => {
const payload = JSON.parse(req.body.toString());
switch (payload.event) {
case 'call_started':
console.log(`📞 Разговор начался: ${payload.call_id}`);
break;
case 'call_ended':
console.log(`✅ Звонок завершён. Длительность: ${payload.duration} сек`);
// Обновляем статус в CRM/БД
break;
case 'call_failed':
console.error(`❌ Неудачный звонок: ${payload.reason}`);
break;
}
res.status(200).send('OK');
});
Задержка перед звонком
Для отложенного звонка используйте очередь задач (bull + Redis) вместо setTimeout — иначе при перезапуске сервера задачи потеряются:
// Добавление задачи с задержкой 5 минут
await callQueue.add(
{ phone, name },
{ delay: 5 * 60 * 1000 } // 300 000 мс
);
Сравнение подходов
| Подход | Сложность | Надёжность | Стоимость |
|---|---|---|---|
| Готовый виджет провайдера | ★☆☆☆☆ (минимум) | ★★★★☆ | От 900 ₽/мес + минуты |
| Свой виджет + облачный API | ★★★☆☆ | ★★★★★ | API-тариф + хостинг (~500 ₽/мес) |
| Свой сервер (Asterisk/FreeSWITCH) | ★★★★★ | ★★★★☆ | VPS + SIP-транк (~1500 ₽/мес) |
Выводы
- Callback-виджет — это бэкенд + интеграция с АТС, а не просто форма на сайте
- Ключевой компонент — SIP-провайдер с API (Манго, Задарма, Твиллио и др.)
- Для продакшена обязательно используйте очередь задач вместо setTimeout
- Вебхуки позволяют отслеживать статус звонка и интегрировать с CRM
- Свой виджет даёт контроль над дизайном и логикой, но требует разработки
Код в статье адаптирован под большинство российских провайдеров (Манго, Задарма). Для зарубежных сервисов (Twilio) меняется только формат API-запроса.
Комментарии
Пока нет комментариев. Будьте первым!