Исходящий обзвон с автоинформатором – простая автоматизация вызовов
Используйте 3CX Call Control API, чтобы автоматизировать исходящий обзвон. Подключите список номеров к IVR, автоматически воспроизводите сообщение и переводите вызовы по разным направлениям в зависимости от выбора абонента в IVR. В отличие от традиционного исходящего обзвона, наш метод обеспечивает гибкость выбора сообщений, расширенную маршрутизацию, интеграции с CRM и базами данных. Читайте дальше, чтобы узнать подробности.
Когда использовать исходящий обзвон
Типичный пример – отмена рейса. Авиакомпания может уведомить пассажиров с помощью записанного сообщения и предоставить опции меню для связи с поддержкой.
Это базовый пример, но его можно расширить. Для исходящих кампаний можно создать собственный IVR с обработкой DTMF-ввода и управлением сообщениями
Другие примеры скриптов смотрите в официальном репозитории 3CX на GitHub.
Настройка обработки вызовов и интеграция по API
Создайте IVR в 3CX, добавьте доступ к Call Control API и выберите этот IVR в списке добавочных номеров.
Инициация вызова
В интерфейсе клиента используется простое текстовое поле для ввода списка номеров через запятую.
const destinations = source
.split(',')
.map((num) => num.trim())
.filter(Boolean);
Очередь обрабатывает вызовы по одному. Неотвеченные или занятые вызовы можно поставить в очередь для повторного набора.
destinations.forEach((destNumber) => this.callQueue.enqueue(destNumber));
Логика обзвона
Функция ниже получает первый номер из очереди и начинает обработку.
public async makeCallsToDst() {
if (this.callQueue.isEmpty()) return;
const destNumber = this.callQueue.dequeue();
// …
Перед набором номера система проверяет соединение с АТС и убеждается, что добавочный номер источника вызова не занят.
if (!this.sourceDn || !this.externalApiSvc.connected) {
if (destNumber)
this.failedCalls.push({
callerId: destNumber,
reason: NO_SOURCE_OR_DISCONNECTED,
});
return;
}
const participants = this.getParticipantsOfDn(this.sourceDn);
if (participants && participants.size > 0) {
if (destNumber)
this.failedCalls.push({
callerId: destNumber,
reason: CAMPAIGN_SOURCE_BUSY,
});
return;
}
//…
Выполнение вызова
Вызов совершается с первого доступного устройства.
Список доступных устройств для конкретного DN можно найти в состоянии Call Control.
try {
const source = this.fullInfo?.callcontrol.get(this.sourceDn);
const device: DNDevice | undefined = source?.devices?.values().next().value;
if (!device?.device_id) {
throw new BadRequest('Devices not found');
}
const response = await this.externalApiSvc.makeCallFromDevice(
this.sourceDn,
encodeURIComponent(device.device_id),
destNumber,
);
//…
Метод makeCallFromDevice использует эту конечную точку:
public makeCallFromDevice(source: string, deviceId: string, dest: string) {
const url = '/callcontrol' + `/${source}` + '/devices' + `/${deviceId}` + '/makecall';
return this.fetch!.post(
url,
{
destination: dest,
},
{
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
},
);
}
Обработка ошибок
Если АТС принимает запрос, Caller ID сохраняется. В противном случае регистрируется ошибка.
if (response.data.result?.id) {
this.incomingCallsParticipants.set(response.data.result.id, response.data.result);
} else {
this.failedCalls.push({
callerId: destNumber!,
reason: response?.data?.reasontext || UNKNOWN_CALL_ERROR,
});
}
//…
Ошибки между приложением и АТС обрабатываются в этом разделе:
//...
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
this.failedCalls.push({
callerId: destNumber!,
reason: error.response?.data.reasontext || UNKNOWN_CALL_ERROR,
});
} else {
this.failedCalls.push({
callerId: destNumber!,
reason: UNKNOWN_CALL_ERROR,
});
}
}
Обработка событий участников вызова
WebSocket-соединение отслеживает статус IVR, запускает новые вызовы и управляет участниками.
Подробнее о событиях WebSocket и других связанных аспектах см. в этом руководстве.
private wsEventHandler = (json: string) => {
try {
const wsEvent: WSEvent = JSON.parse(json);
if (!this.externalApiSvc.connected || !wsEvent?.event?.entity) {
return;
}
const { dn, type } = determineOperation(wsEvent.event.entity);
//...
При обновлении приложение получает и сохраняет новые данные.
case EventType.Upset:
{
this.externalApiSvc
.requestUpdatedEntityFromWebhookEvent(wsEvent)
.then((res) => {
const data = res.data;
set(this.fullInfo, wsEvent.event.entity, data); // update local state
if (dn === this.sourceDn) {
if (type === PARTICIPANT_TYPE_UPDATE) {
/**
* handle here update of participants
*/
}
}
})
.catch((err) => {
if (axios.isAxiosError(err)) {
console.error(`AXIOS ERROR code: ${err.response?.status}`);
} else console.error('Unknown error', err);
});
}
break;
Можно использовать этот URL для запроса обновленной сущности и выполнения инкрементного обновления состояния для нашего приложения (см. DN Update Request):
public requestUpdatedEntityFromWebhookEvent(ws: WSEvent) {
return this.fetch.get(ws.event.entity);
}
Когда участник удаляется, кампания продолжается.
case EventType.Remove: {
const removed = set<CallParticipant>(this.fullInfo, wsEvent.event.entity, undefined);
if (dn === this.sourceDn) { // update related to our campaign handler
if (type === PARTICIPANT_TYPE_UPDATE) {// update related to call participant
/**
* handle here removed participants
*/
if (removed?.id) {
//...
if (!participants || participants?.size < 1) { // Handler is free
this.makeCallsToDst(); // continue with campaign
}
}
}
}
}
Мы можем использовать этот обработчик событий в WebSocket event listener.
ws.on('message', (buffer) => {
const message = decoder.decode(buffer as Buffer);
wsEventHandler(message);
});
Другие сценарии 3CX Call Flow
На нашем сайте доступна коллекция сценариев Call Flow. Ознакомьтесь с ними – и вы сможете автоматизировать 3CX для ваших потребностей.



