Категории

Callback-виджет: как реализовать двусторонний автоотзвон на сайте

29.01.2026 | Статья из категории: Создание сайтов

Как реализовать двусторонний автоотзвон на сайте API Callback виджет

Одна из самых эффективных техник конверсии — мгновенный звонок клиенту после заполнения формы. Но настоящая магия происходит, когда система соединяет клиента напрямую с менеджером без ожидания в очереди. Разбираем архитектуру и код.

TL;DR: Для двустороннего звонка нужен SIP-провайдер + бэкенд-логика + вебхуки. Клиент и менеджер звонят «друг другу» через виртуальную АТС, которая мгновенно соединяет каналы.

Как это работает: архитектура

Клиент (браузер)
       │
       ▼ 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
  • Свой виджет даёт контроль над дизайном и логикой, но требует разработки
Совет: Начните с тестового тарифа провайдера (обычно есть бесплатные минуты). Реализуйте минимальный рабочий прототип за 1-2 дня, затем добавляйте аналитику и интеграции.

Код в статье адаптирован под большинство российских провайдеров (Манго, Задарма). Для зарубежных сервисов (Twilio) меняется только формат API-запроса.

Комментарии

Пока нет комментариев. Будьте первым!

Оставить комментарий

← Назад к списку статей

Посетителей сегодня: 0
о блоге

© Digital Specialist | Не являемся сотрудниками Google, Яндекса и NASA