Исходящий обзвон с автоинформатором – простая автоматизация вызовов

Используйте 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 для ваших потребностей.