Вы можете настроить интеграцию с провайдерами IP-телефонии, чтобы сотрудники могли совершать и принимать звонки от клиентов. Для этого используйте:
- готовые модули из каталога ELMA365 Store: Гравител, SIPNET, Zadarma, MANGO OFFICE, Sipuni, Билайн, Asterisk (доступен только для ELMA365 On-Premises);
- пользовательский модуль. Чтобы принимать и инициировать вызовы, нужно соединить модуль с провайдером.
Начало внимание
Функциональные возможности телефонии доступны при активации одного из платных решений CRM, в состав которого входит решение ELMA365 Управление коммуникациями.
Конец внимание
Чтобы настроить интеграцию с помощью пользовательского модуля:
- Выполните предварительные настройки, например, проверьте доступ к аккаунту телефонии, установите SIP-софтфон и т. д.
- Создайте модуль и в его скриптах пропишите методы Web API, чтобы получить сгенерированный Webhook и токен для обмена данными с провайдером телефонии.
- Соедините модуль с провайдером — в настройках на стороне провайдера укажите токен и Webhook, а в модуль скопируйте ключ авторизации и URL телефонной станции.
Чтобы пользователи могли обработать звонок в его карточке, например принять или переназначить вызов и т. д., добавьте в скрипты модуля соответствующие методы Web API.
В статье рассмотрен пример интеграции с телефонией Гравител.
Подробнее о настройке интеграции при помощи пользовательского модуля читайте в справке ELMA365 TS SDK.
Предварительные настройки для интеграции с телефонией
Перед началом настройки:
- Проверьте, что ваш аккаунт у выбранного провайдера телефонии активен.
- Ознакомьтесь с условиями и сроком хранения записи звонка на сервере провайдера. В течение этого срока в интерфейсе ELMA365 пользователь может прослушать и загрузить запись себе на компьютер. Если запись удалена, появится сообщение об ошибке.
- Убедитесь, что в настройках провайдера можно указать Webhook системы. С его помощью будут обрабатываться HTTP-запросы о событиях телефонии. Если такой возможности нет, создайте промежуточный сервис, который преобразует данные провайдера в запрос на сервер ELMA365. Подробнее читайте в статье «Переносимые сервисы в модулях».
- Для совершения и приёма звонков на рабочих местах сотрудников убедитесь, что провайдер предоставляет SIP-софтфон. В противном случае установите облачную или локальную программу, которая поддерживает SIP-протокол и обеспечивает голосовую связь, например, MicroSip.
- Чтобы использовать телефонию для работы с продажами, активируйте одно из системных решений CRM. Без лицензии функциональные возможности CRM недоступны.
Создать пользовательский модуль интеграции с телефонией
- Перейдите в раздел Администрирование > Модули и в правом верхнем углу нажмите кнопку + Модуль.
- В открывшемся окне выберите опцию Создать, введите название и описание модуля. Нажмите кнопку Создать.
- На странице управления модулем перейдите на вкладку Методы API и нажмите кнопку Редактировать. На открывшейся странице перейдите на вкладку Скрипты.
- Вставьте код-заготовку для методов, которые реализуют интеграцию и обмен данными с телефонией.
// Проверить соединение с телефонией
async function VoipTestConnection(): Promise<VoipTestConnectionResult> {
return {
success: false,
failReason: 'Функция проверки соединения не реализована.',
};
}
// Обработать запрос от провайдера IP-телефонии
async function VoipParseWebhookRequest(request: FetchRequest): Promise<VoipWebhookParseResult> {
return {
response: new HttpResponse()
.status(400)
.content('Hello, world!'),
};
}
// Получить список пользователей IP-телефонии
async function VoipGetMembers(): Promise<VoipMember[]> {
throw new Error('Функция не реализована.');
}
// Сгенерировать звонок
async function VoipGenerateCall(srcPhone: string, dstPhone: string): Promise<void> {
throw new Error('Функция не реализована.');
}
// Получить ссылку на запись звонка
async function VoipGetCallLink(callData: any): Promise<string> {
throw new Error('Функция не реализована.');
}
- Сохраните и опубликуйте изменения.
На странице модуля отобразятся разделы:
- Настройки телефонии, где указаны автоматически сгенерированные токен и Webhook;
- Настройки обработки входящего звонка, где по умолчанию выбрано приложение для хранения контактных данных клиентов.
Соединить модуль с провайдером телефонии
Рассмотрим настройку интеграции пользовательского модуля с провайдером на примере телефонии Гравител.
- На странице модуля перейдите на вкладку Настройки и добавьте два строковых поля Ключ для авторизации в облачной АТС (
apiKey
) и Адрес облачной АТС (endpoint
). В эти поля позднее вы запишете данные из настроек провайдера для подключения к Гравител.
- Вернитесь на страницу модуля — на ней появятся добавленные в предыдущем пункте поля. Cкопируйте значения полей Webhook URL и Токен.
- Перейдите в панель управления Гравител. Интеграция настраивается по Web API Гравител предыдущего поколения. В зависимости от версии личного кабинета, которую вы используете, выполните следующие шаги:
- для актуальной версии — перейдите в Интеграции и выберите блок API Гравител. Нажмите Установить;
- для предыдущей версии — перейдите в Интеграция с CRM и на открывшейся странице в правом нижнем углу выберите Интеграции с вашей CRM.
- На открывшейся странице заполните поля Название интеграции, Ваша CRM, Адрес вашей CRM и Ключ для авторизации в вашей CRM значениями из настроек модуля.
Затем скопируйте значения полей Ключ для авторизации в Облачной АТС и Адрес Облачной АТС. Перейдите в ELMA365 и вставьте их в соответствующие поля на странице модуля.
Подробнее о настройке интеграции в личном кабинете Гравител читайте в статье «Телефония Гравител».
- Сохраните настройки в панели управления Гравител и на странице модуля.
- Перейдите на страницу управления модулем и откройте вкладку Методы API > Скрипты. Нажмите кнопку Редактировать и добавьте в начало скрипта следующий код:
// Загрузить значение поля Ключ для авторизации в облачной АТС из настроек модуля
function getApiKey() {
const apiKey = Namespace.params.data.apiKey;
if (!apiKey) {
throw new Error('invalid API key');
}
return apiKey;
}
// Загрузить значение поля Адрес облачной АТС из настроек модуля
function getEndpoint() {
const endpoint = Namespace.params.data.endpoint;
if (!endpoint) {
throw new Error('invalid endpoint URL');
}
return endpoint;
}
// Выполнить запрос к Гравител
async function fetchEndpoint(command: string, body: Object = {}): Promise<FetchResponse> {
return fetch(getEndpoint(), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
cmd: command,
token: getApiKey(),
...body,
}),
});
}
// Распарсить контент с типом application/x-www-form-urlencoded
function parseUrlEncoded(body: string): Record<string, string> {
const result: Record<string, string> = {};
for (const pair of body.split('&')) {
const keyValue = pair.split('=');
if (keyValue.length === 2) {
const key = decodeURIComponent(keyValue[0]);
const value = decodeURIComponent(keyValue[1]);
result[key] = value;
}
}
return result;
}
// Сообщение от Гравител
interface GravitelWebhookRequest {
cmd: string; // Тип операции
type: string; // Тип события, связанного со звонком
phone: string; // Номер телефона клиента (в формате E.164)
ext?: string; // Внутренний номер пользователя облачной АТС, если есть
}
// Данные, передающиеся с командой history
interface GravitelWebhookHistoryCommand extends GravitelWebhookRequest {
duration: string; // Общая длительность звонка в секундах
callid: string; // Уникальный id звонка
link?: string; // Ссылка на запись звонка, если она включена в Облачной АТС
status: string; // Статус входящего или исходящего звонка
}
// Данные, которые сохраняются с каждым звонком
interface GravitelCallData {
id: string;
link: string;
}
// Сконвертировать статус звонка Гравител в статус звонка ELMA365
function toCallDisposition(status: string): VoipCallDisposition {
switch (status) {
case 'Success':
return VoipCallDisposition.Answered;
case 'Busy':
return VoipCallDisposition.Busy;
case 'Missed':
return VoipCallDisposition.Cancel;
case 'NotAvailable':
return VoipCallDisposition.NoAnswer;
case 'NotAllowed':
default:
return VoipCallDisposition.Unknown;
}
}
- Добавьте или замените метод
VoipTestConnection
, который позволяет проверить соединение с телефонией. В данном примере тестируется команда для получения списка пользователей. Вы можете указать другую команду.
// Проверить соединение к телефонии
async function VoipTestConnection(): Promise<VoipTestConnectionResult> {
try {
// Выполняем команду для получения списка пользователей телефонии
const response = await fetchEndpoint('accounts');
const message = await response.json();
if (response.status !== 200) {
throw new Error(`${response.status} ${response.statusText}: ${message.error}`);
}
return { success: true };
} catch (e) {
return {
success: false,
failReason: e.message,
};
}
}
- Сохраните и опубликуйте скрипты, после чего перейдите на страницу модуля и нажмите кнопку Проверить соединение. Появится сообщение об успешном подключении к телефонии.
Если вы видите ошибку при проверке соединения:
- убедитесь, что поля на странице модуля и в панели управления Гравител заполнены корректно. Например, ошибка «Invalid token» означает, что неверно заполнено поле Токен;
- проверьте, что скрипты составлены без ошибок, а также успешно сохранены и опубликованы;
- убедитесь, что ваш аккаунт Гравител активен.
- После этого добавьте или замените следующие методы для интеграции с Гравител:
- метод
VoipParseWebhookRequest
— позволяет модулю обработать данные из HTTP-запроса от провайдера и передать их в ELMA365;
// Обработать запрос от провайдера IP-телефонии
async function VoipParseWebhookRequest(request: FetchRequest): Promise<VoipWebhookParseResult> {
try {
const headers = request.headers;
const contentType = headers ? headers['Content-Type'] : undefined;
if (contentType !== 'application/x-www-form-urlencoded') {
throw new Error(`Expected Content-Type to be "application/x-www-form-urlencoded" (got: "${contentType}"`);
}
if (typeof request.body !== 'string') {
throw new Error('Expected request body to be string')
}
let event: VoipWebhookRequest | undefined;
let callRecord: VoipCallRecord | undefined;
let response: HttpResponse | undefined;
const data = <GravitelWebhookRequest><unknown> parseUrlEncoded(request.body);
switch (data.cmd) {
case 'event': {
// Облачная АТС отправляет в вашу CRM уведомления о событиях входящих звонков пользователям:
появлении, принятии или завершении звонка. Команда может быть использована для отображения
всплывающей карточки клиента в интерфейсе CRM
const dstPhone = data.ext ?? '';
switch (data.type) {
case 'INCOMING': {
// Пришёл входящий звонок. В это время у менеджера должен начать звонить телефон
event = {
event: VoipWebhookEvent.NotifyStart,
direction: VoipCallDirection.In,
dstPhone: dstPhone,
srcPhone: data.phone,
disposition: VoipCallDisposition.Unknown,
};
} break;
case 'ACCEPTED': {
// Звонок успешно принят, т. е. менеджер снял трубку. В этот момент можно убрать всплывающую
карточку контакта в CRM
event = {
event: VoipWebhookEvent.NotifyAnswer,
direction: VoipCallDirection.In,
dstPhone: dstPhone,
srcPhone: data.phone,
disposition: VoipCallDisposition.Unknown,
}
} break;
case 'COMPLETED': {
// Звонок успешно завершён, т. е. менеджер или клиент положили трубку после разговора
event = {
event: VoipWebhookEvent.NotifyEnd,
direction: VoipCallDirection.In,
dstPhone: dstPhone,
srcPhone: data.phone,
disposition: VoipCallDisposition.Unknown,
}
} break;
case 'CANCELLED': {
// Звонок сброшен, т.е. клиент не дождался пока менеджер снимет трубку. Либо, если это был
звонок сразу на группу менеджеров, на звонок мог ответить кто-то ещё
event = {
event: VoipWebhookEvent.NotifyEnd,
direction: VoipCallDirection.In,
dstPhone: dstPhone,
srcPhone: data.phone,
disposition: VoipCallDisposition.Cancel,
}
} break;
case 'OUTGOING': {
// Менеджер совершает исходящий звонок. В это время облачная АТС пытается дозвониться до клиента
event = {
event: VoipWebhookEvent.NotifyStart,
direction: VoipCallDirection.Out,
dstPhone: dstPhone,
srcPhone: data.phone,
disposition: VoipCallDisposition.Unknown,
}
} break;
default: throw new Error(`Unknown event type "${data.type}"`)
}
} break;
case 'history': {
// После успешного звонка в CRM отправляется запрос с данными о звонке и ссылкой на запись
разговора. Команда может быть использована для сохранения в данных ваших клиентов истории
и записей входящих и исходящих звонков
const cmd = <GravitelWebhookHistoryCommand> data;
callRecord = {
srcPhone: cmd.phone,
dstPhone: cmd.ext ?? '',
direction: cmd.type === 'out' ? VoipCallDirection.Out : VoipCallDirection.In,
duration: parseInt(cmd.duration),
// Данные из этого поля будут доступны в функции VoipGetCallLink
call: <GravitelCallData>{
link: cmd.link,
id: cmd.callid,
},
disposition: toCallDisposition(cmd.status),
}
} break;
}
return {
event: event,
callRecord: callRecord,
response: response,
};
} catch (e) {
return {
response: new HttpResponse()
.status(400)
.content(JSON.stringify({
error: e.message ?? 'internal error',
}))
};
}
}
- метод
VoipGetMembers
— позволяет получить данные о пользователях телефонии. Эти данные отображаются на странице модуля при сопоставлении пользователей провайдера и пользователей ELMA365;
// Получить список пользователей от провайдера IP-телефонии
async function VoipGetMembers(): Promise<VoipMember[]> {
const response = await fetchEndpoint('accounts');
if (response.status !== 200) {
throw new Error(`received error response ${response.status}: ${response.statusText}`);
}
interface GravitelVoipUser {
name: string;
ext: string;
}
const voipUsers = <GravitelVoipUser[]> (await response.json());
return voipUsers.map(user => ({
id: user.ext,
label: user.name,
}));
}
- метод
VoipGenerateCall
— позволяет инициировать звонок из интерфейса ELMA365;
// Сгенерировать звонок
async function VoipGenerateCall(srcPhone: string, dstPhone: string): Promise<void> {
const response = await fetchEndpoint('makeCall', {
phone: dstPhone,
user: srcPhone,
});
if (response.status !== 200) {
throw new Error(`received error response ${response.status}: ${response.statusText}`);
}
}
- метод
VoipGetCallLink
— позволяет сохранить запись разговора. В качестве аргумента передаются данные из поля call параметраVoipCallRecord
(заполняется в методеVoipParseWebhookRequest
).
// Получить ссылку на запись разговора
async function VoipGetCallLink(callData: GravitelCallData): Promise<string> {
return callData.link;
}
- Чтобы реализовать функциональность кнопок для обработки звонка в его карточке, добавьте методы, перечисленные в справке ELMA365 TS SDK.
- Сохраните и опубликуйте скрипты.
- На странице модуля нажмите на появившуюся кнопку Настроить и сопоставьте номера пользователей из настроек провайдера с пользователями ELMA365. Подробнее читайте в статье «Телефония Гравител».
- Сохраните настройки.
Теперь интеграция с Гравител полностью функционирует.