Локальный REST API
У AI Softphone есть REST API для CTI-интеграции: программа на том же компьютере может совершать звонки и управлять ими, читать контакты, историю звонков и SIP-учётные записи и следить за идущими звонками. Никакого SDK, никакого облачного посредника и никакого порта, открытого в сеть. Запросы и ответы — JSON, так что хватит curl или любого HTTP-клиента.
После установки API выключен: пока вы его не включите, ничего не слушает. Включённый, он слушает только петлевой интерфейс — небольшой веб-интерфейс, отвечающий только этому компьютеру, — и недоступен из офисной сети, через VPN или с другой машины.
Используйте API, когда вашей программе нужны данные телефона или нужно управлять звонком. Используйте вебхуки, когда нужно реагировать на звонки по мере их хода, без опроса. Большинство интеграций используют и то и другое; они не зависят друг от друга.
Включение
Откройте Настройки → Интеграция и перейдите к Локальному управлению.
- Включите Разрешить другим программам на этом компьютере управлять телефоном. Сервер запускается сразу.
- Оставьте Порт по умолчанию,
8377, если его не занимает другая программа. - При желании задайте Токен. После сохранения поле показывает Сохранено — введите, чтобы заменить.
- В Доступе выберите, какие группы открыть: Контакты, История звонков, Звонки и управление ими, Учётные записи, Настройки, Счётчики (метрики). Выключенная группа не фильтруется, а не обслуживается вовсе.
- Проверьте:
curl http://127.0.0.1:8377/accounts. Если в ответ пришёл JSON, API работает.
Отдельная служба не устанавливается, перезапуск не нужен. Часть программы, которая это делает, отключается в Модулях (Интеграция).
Собственная страница API
Открыть страницу API открывает http://127.0.0.1:8377 в браузере. По этому адресу отвечает список всего, что обслуживает API, на английском; адреса, которые что-то читают, — ссылки, по ним можно перейти.
Доступ и токен
Что может программа, зависит от того, меняет ли запрос сохранённые данные, а не от того, читает ли он:
- Без токена любая программа на компьютере может читать всё в открытых группах и управлять звонками: звонить, отвечать, класть трубку, удерживать, снимать с удержания, переводить и отправлять DTMF.
- С токеном в заголовке
Authorizationей доступны ещё и точки, меняющие сохранённое. Без токена эти точки не обслуживаются и не показываются на странице API.
Токен хранится в хранилище ключей компьютера, а не в файле настроек, и никогда не возвращается /settings.
Без токена любая программа, запущенная на этом компьютере, может управлять телефоном, в том числе отвечать на звонки. На личном рабочем месте это обычно приемлемо. На общей или управляемой машине задайте токен и обращайтесь с ним, как с любым паролем.
Точки доступа
Базовый адрес — http://127.0.0.1:8377. Точкам ниже токен не нужен.
| Метод | Путь | Что делает |
|---|---|---|
| GET | /metrics | Счётчики в формате Prometheus. |
| GET | /ui | Список записей в виде HTML-страницы. |
| GET | /ui/recordings/{id} | Запись с расшифровкой в виде HTML-страницы. |
| GET | /ui/recordings/{id}/audio | Звук для страницы выше. |
| GET | /contacts | Контакты. |
| GET | /contacts/{id} | Один контакт. |
| GET | /history | Журнал звонков, новые сверху. Принимает ?limit=, ?missed=true и ?declined=true. |
| GET | /calls | Идущие звонки. |
| POST | /calls | Совершает звонок: {"number": "...", "account_id": "..."}. |
| POST | /calls/{id}/answer | Отвечает на звонок. |
| POST | /calls/{id}/hangup | Кладёт трубку. |
| POST | /calls/{id}/hold | Ставит звонок на удержание. |
| POST | /calls/{id}/resume | Снимает с удержания. |
| POST | /calls/{id}/dtmf | Отправляет тоны: {"digits": "..."}. |
| POST | /calls/{id}/transfer | Переводит звонок: {"target": "..."}. |
| GET | /accounts | SIP-учётные записи и состояние их регистрации. Никогда — пароль. |
| GET | /settings | Вся конфигурация без секретов. |
| GET | /taxonomy | Категории, метки и красные флаги с их кодами. |
Каждый идентификатор — UUID, выданный телефоном: id звонка берётся из /calls или из ответа на POST /calls, id учётной записи — из /accounts.
Имена полей — в snake_case, а окончание говорит о типе: _id — ссылка на UUID, _ts — момент в миллисекундах Unix (UTC), _s — длительность в секундах. То же действует для вебхуков; свои имена есть только у /settings. В REST API эти значения — числа JSON, а неизвестный момент — null.
Пример: звонок
POST /calls совершает исходящий звонок. Тело — JSON с номером number и, при желании, account_id учётной записи, с которой звонить:
curl --location 'http://127.0.0.1:8377/calls' \
--header 'Content-Type: application/json' \
--data '{
"number": "1020",
"account_id": "0d7a4c52-6f2e-4a51-8f46-7d9a3e1b2c90"
}'
В ответ приходит идентификатор нового звонка:
{ "id": "9a3e5c71-2d48-4b6f-8e10-3c5f7a1b9d24" }
numberобязателен. Без него ответ —400 {"error":"a call needs a number"}, и ничего не набирается.- Номер дополняется на выбранной учётной записи так же, как его дополняет наборник:
1020уходит какsip:1020@pbx.example.com.POST /calls/{id}/transferдополняет свойtargetтак же; цель, в которой уже есть схема или@, отправляется как есть. account_idнеобязателен; берите его изGET /accounts. Без него звонок идёт с учётной записи, выбранной в главном окне.- Используйте
idв/calls/{id}/…:hangup,hold,resume,dtmfиtransfer. Вебхуки этого звонка несут тот жеid.
С веб-страницы: звонок по клику
Страница, обращающаяся к 127.0.0.1, попадает на компьютер, где работает браузер, — тот же, где работает телефон, — поэтому кнопке «позвонить» в CRM собственный сервер не нужен:
async function dial(number) {
const r = await fetch("http://127.0.0.1:8377/calls", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ number }),
});
if (!r.ok) console.warn("softphone:", (await r.json()).error);
}
Что содержат ответы
Идущие звонки: GET /calls
У каждого звонка есть id, seance_id, account_id, direction, state, number, name, uri, dialed, muted, event_ts, callstart_ts и callstate_ts.
state—dialing,ringing-out,ringing-in,active,hold(удержан этим телефоном),onhold(удержан другой стороной),conferenceилиended. Если подходит несколько,conferenceважнееhold, аholdважнееonhold.muted— выключен ли микрофон в звонке; выключение микрофонаstateне меняет.seance_id— разговор: звонки, связанные переводом, консультацией или конференцией, разделяют его.event_ts— когда был сформирован ответ. Сравните его сcallstate_ts, чтобы узнать, сколько звонок в своём состоянии, не полагаясь на собственные часы.
Учётные записи: GET /accounts
У каждой учётной записи есть id (везде в других местах — account_id), её настройки — transport (udp, tcp или tls), port, registrar, outbound_proxy, expiry_s и другие, — включена ли она (enabled) и её состояние на АТС (state): registered, пока линия работает. Пароли не включаются никогда.
История звонков: GET /history
Новые сверху, 100 записей, если ?limit= не говорит иного. ?missed=true возвращает только пропущенные звонки, ?declined=true — только отклонённые этим телефоном.
| Поле | Значение |
|---|---|
id | Собственный идентификатор записи журнала. Это не id звонка из /calls и вебхуков; связывает их seance_id. |
outcome | Основная классификация: answered, missed, declined или failed. |
answered | true или false. |
duration_s | 0 для звонка, который так и не соединился. |
number, uri | Собеседник — номером и SIP-адресом. |
name | Из Контактов, если номер известен, иначе пусто. Сопоставляйте по number, а не по этому полю. |
dialed | Набранные цифры — для исходящего звонка; для входящего пусто. |
account, account_id | Линия, на которой был звонок. |
reason | Чем закончился: local-hangup, remote-hangup, cancelled… |
answered_by | no, если ответил человек; иначе — что ответило на звонок. |
Контакты: GET /contacts
У каждого контакта есть id, name, number и линия, к которой он относится, — account_id и account; пустой account означает, что контакт не привязан к линии.
Записи
Записи и расшифровки не отдаются в JSON. API обслуживает их HTML-страницами /ui и /ui/recordings/{id}: ставьте в CRM ссылку на эти страницы вместо того, чтобы перекладывать звук. Ссылка открывается на компьютере, где хранится запись, и звук его не покидает.
Справочники и настройки
У каждой записи /taxonomy есть постоянный code, title и description на языке интерфейса, kind (category, tag или red_flag) и, для красных флагов, severity. Сопоставляйте по code, никогда — по title: названия приходят на языке, выбранном в телефоне (при русском интерфейсе — Продажи, Поддержка…). Запись с retired: true сохраняется, чтобы старые звонки по-прежнему разрешались, но новым звонкам больше не даётся. Загрузите справочники один раз при запуске, чтобы сопоставить слова телефона с полями вашей системы.
/settings возвращает конфигурацию без секретов: звуковые устройства и громкость, приоритет кодеков, оформление и язык, запуск, горячие клавиши, уровень диагностики и состояние обеих интеграций — это удобно для инструмента поддержки, которому нужно проверить рабочее место без показа экрана. api.disabled перечисляет выключенные группы доступа, webhooks.silenced — выключенные события; пустые списки означают, что включено всё. Пароль SIP, токен API и значение заголовка вебхука в ответ не попадают никогда, как и ключи распознавателей и моделей.
Ошибки
Каждая ошибка — JSON с единственным ключом error, рассчитанный на людей, а не на разбор программой. Тексты ошибок — на английском.
| Статус | Тело | Значение |
|---|---|---|
| 404 | {"error":"no such endpoint"} | Такого пути нет или его группа доступа выключена; ответ одинаковый намеренно. |
| 404 | {"error":"no contact with that id"} | Путь верный, идентификатор — нет. |
| 400 | {"error":"no call with that id"} | Звонок закончился или его не было. |
| 400 | {"error":"a call needs a number"} | POST /calls без номера. Ничего не набрано. |
| 400 | {"error":"no digits to send"} | POST /calls/{id}/dtmf без цифр. |
| 400 | {"error":"a transfer needs a target"} | POST /calls/{id}/transfer без цели. |
| 400 | {"error":"the account this call is on is no longer set up"} | Учётную запись звонка удалили во время звонка, поэтому цель нельзя дополнить. На АТС ничего не отправлено. |
Отклонённые запросы считаются в api_requests_refused_total, так что тихо падающая интеграция видна в метриках, а не только в ваших собственных журналах.
Метрики
GET /metrics возвращает все счётчики телефона, каждый с пояснением. Собирайте их Prometheus или читайте вручную.
| Счётчик | Что считает |
|---|---|
calls_incoming_total | Принятые входящие звонки. |
calls_outgoing_total | Совершённые исходящие звонки. |
calls_answered_total | Отвеченные звонки. |
calls_missed_total | Входящие звонки, на которые не ответили. |
calls_declined_total | Звонки, отклонённые здесь или другой стороной. |
calls_failed_total | Звонки, которые не удалось установить. |
registrations_succeeded_total | Успешные SIP-регистрации. |
registrations_failed_total | SIP-регистрации, отклонённые или не дождавшиеся ответа. |
webhooks_delivered_total | Вебхуки, принятые получателем. |
webhooks_failed_total | Вебхуки, отклонённые или не доставленные. |
webhooks_dropped_total | Вебхуки, выброшенные, потому что очередь была полна. |
api_requests_total | Запросы, обработанные API. |
api_requests_refused_total | Отклонённые запросы: неверный токен, выключенная группа или неизвестный путь. |
Обновление старой интеграции
Ранние версии использовали имена в camelCase и короткие идентификаторы. accountId теперь account_id, startedAt — callstart_ts, durationSeconds — duration_s, answeredBy — answered_by, а поле вебхука at — event_ts. Звонки и учётные записи идентифицируются только UUID: runtimeId и идентификаторы вида call-3 или account-2 больше не возвращаются и не принимаются.
Если не работает
| Признак | Что проверить |
|---|---|
Соединение с 127.0.0.1:8377 отклонено | Локальное управление выключено, телефон не запущен или порт изменён. |
404 {"error":"no such endpoint"} для пути с этой страницы | Выключена его группа доступа. |
| Чтение работает, запись отклоняется | Точкам, меняющим сохранённые данные, нужен токен в заголовке Authorization. |
| Названия категорий не на английском | Названия следуют языку интерфейса. Сопоставляйте по code из /taxonomy. |
Нет accountId, startedAt или at | Интеграция написана под старые имена; см. выше. |
Если проблема с самой регистрацией или звонком, откройте Диагностику.