Клиентский API звонков 4phone
Это руководство описывает production API для выгрузки звонков и записей, настройку API ключей, webhook события и типовой порядок подключения клиентской CRM, ERP, BI или внутренней системы.
1. Возможности
Интеграция поддерживает два способа получения данных:
- REST API для запроса списка звонков, карточки звонка и файла записи.
- Webhooks для автоматического уведомления о завершении звонка и готовности файла записи.
Рекомендуемая схема работы:
- Получать новые события через webhooks.
- Сохранять звонок у себя по полю
id. - Скачивать запись после
CALL_RECORDING_READY. - Периодически сверять данные через REST API, чтобы восстановить события, пропущенные из-за недоступности клиентского endpoint.
2. Адреса
Базовый адрес REST API:
https://api.4phone.uz/api/v1/external
Адрес раздела API ключей:
https://4phone.uz/dashboard/settings/integrations/api-keys
Адрес раздела webhooks:
https://4phone.uz/dashboard/settings/integrations/webhooks
Адрес истории доставок:
https://4phone.uz/dashboard/settings/integrations/deliveries
Swagger поддерживается приложением, но в production по умолчанию отключен.
Маршрут /api/docs появляется только при включенном SWAGGER_ENABLED.
Клиентские интеграции должны опираться на это руководство и стабильный API v1.
3. Создание API ключа
Создавать и отзывать ключи могут пользователи с ролью OWNER, ADMIN или
SUPERADMIN.
Порядок настройки:
- Откройте
Настройки. - Перейдите в
Интеграции. - Откройте
API ключи. - Нажмите
Создать ключ. - Укажите понятное название, например
Bitrix exportилиBI production. - Выберите scope
calls:read. - Выберите scope
recordings:read, если клиенту нужны файлы записей. - При необходимости ограничьте ключ отделом, IP адресами и сроком действия.
- Создайте ключ и сразу сохраните показанный секрет.
Полное значение ключа показывается только один раз. В базе хранится только SHA-256 хеш. Восстановить потерянный секрет нельзя. Нужно отозвать старый ключ и создать новый.
Для каждой внешней системы рекомендуется создавать отдельный ключ. Это позволяет независимо менять доступ, IP ограничения и срок действия.
3.1. Scopes
Для API звонков используются два разрешения:
calls:readразрешает список и карточку завершенных звонков.recordings:readразрешает проверку и скачивание файлов записей.
Ключ только с recordings:read может скачать запись по известному id, но не
может запросить список звонков. В обычной интеграции выбирайте оба scope.
3.2. Ограничение по отделу
Если поле Отдел не заполнено, ключ получает доступ ко всем разрешенным
звонкам своего тенанта.
Если выбран отдел, API возвращает только звонки добавочных номеров этого
отдела. Запрос добавочного другого отдела вернет пустой список. Карточка чужого
звонка вернет 404.
Ограничение отдела применяется и к скачиванию записи.
3.3. IP whitelist
Поле IP whitelist принимает:
- отдельный IPv4 адрес, например
45.147.253.34; - IPv4 подсеть, например
45.147.253.32/27; - отдельный IPv6 адрес;
- IPv6 подсеть в CIDR формате.
Значения можно разделять запятой, пробелом или переносом строки. Если список пуст, ограничение по IP не применяется.
Указывайте публичный исходящий IP сервера, который обращается к API. При
несовпадении API вернет 403 с кодом api_key.ip_not_allowed.
3.4. Срок действия и отзыв
Срок действия необязателен, но рекомендуется для временных интеграций и тестовых сред.
Отозванный или просроченный ключ больше не принимается. Отзыв необратим. Создание и отзыв ключей записываются в аудит платформы.
4. Авторизация запросов
Рекомендуемый заголовок:
X-Api-Key: cpbx_XXXXXXXX_SECRET
Также поддерживается Bearer формат:
Authorization: Bearer cpbx_XXXXXXXX_SECRET
Не передавайте ключ:
- в query параметрах URL;
- в имени файла;
- в клиентском JavaScript браузера;
- в сообщениях об ошибках и открытых логах.
Храните ключ в secret manager или переменной окружения:
export FOURPHONE_API_KEY='cpbx_XXXXXXXX_SECRET'
Проверка доступа:
curl -sS \
-H "X-Api-Key: $FOURPHONE_API_KEY" \
"https://api.4phone.uz/api/v1/external/calls?limit=1"
5. Список звонков
GET /calls
Полный адрес:
https://api.4phone.uz/api/v1/external/calls
Требуемый scope: calls:read.
API возвращает только завершенные входящие и исходящие звонки. Внутренние звонки не входят в клиентскую выгрузку.
Доступные статусы:
ANSWEREDBUSYNO_ANSWERFAILEDCANCELLED
5.1. Фильтры
Поддерживаются параметры:
directionсо значениемINBOUNDилиOUTBOUND;statusсо списком статусов через запятую;startedFromс началом периода в RFC 3339, граница включается;startedToс концом периода в RFC 3339, граница не включается;phoneс номером или частью номера;extensionNumberс добавочным номером;hasRecordingсо значениемtrueилиfalse;limitот 1 до 100, значение по умолчанию 50;cursorиз поляmeta.nextCursorпредыдущего ответа.
В фильтре phone все символы, кроме цифр, удаляются. Поиск выполняется по
номеру звонящего и номеру назначения.
hasRecording=true означает, что у звонка есть ссылка на запись. Файл может
еще обрабатываться. Перед скачиванием проверяйте
recording.available=true.
Пример запроса входящих отвеченных звонков:
curl -sS \
-H "X-Api-Key: $FOURPHONE_API_KEY" \
"https://api.4phone.uz/api/v1/external/calls?direction=INBOUND&status=ANSWERED&startedFrom=2026-07-01T00%3A00%3A00%2B05%3A00&limit=50"
Пример поиска по номеру:
curl -sS \
-H "X-Api-Key: $FOURPHONE_API_KEY" \
"https://api.4phone.uz/api/v1/external/calls?phone=998901234567&limit=50"
5.2. Формат ответа
{
"data": [
{
"id": "847cc73c-6aaf-4a5e-aea8-d106fae10c45",
"callId": "17ed2d85-a64a-4ce0-9151-a4dc57505dc6",
"direction": "INBOUND",
"status": "ANSWERED",
"date": "23.07.2026 10:40:59",
"dateIso": "2026-07-23T05:40:59Z",
"timeZone": "Asia/Tashkent",
"from": "998901234567",
"to": "781139788",
"durationSeconds": 83,
"extensionNumber": "101",
"recording": {
"status": "AVAILABLE",
"available": true,
"fileName": "17ed2d85-a64a-4ce0-9151-a4dc57505dc6.wav",
"contentType": "audio/wav",
"sizeBytes": 1382400,
"downloadUrl": "/api/v1/external/calls/847cc73c-6aaf-4a5e-aea8-d106fae10c45/recording"
}
}
],
"meta": {
"limit": 50,
"hasMore": false,
"nextCursor": null
}
}
5.3. Значение полей
idявляется ID записи звонка. Используйте его для карточки и скачивания записи.callIdявляется техническим ID телефонной сессии.directionсодержитINBOUNDилиOUTBOUND.statusсодержит итоговый статус.dateсодержит локальную дату тенанта с точностью до секунды.dateIsoсодержит то же время в UTC и RFC 3339.timeZoneуказывает часовую зону, использованную дляdate.durationSecondsсодержит чистое время разговора в секундах. Для звонка без ответа значение равно 0.extensionNumberсодержит добавочный номер сотрудника илиnull.
Для входящего звонка:
fromсодержит внешний номер звонящего без знака+;toсодержит исходный DID, на который поступил звонок.
Для исходящего звонка:
fromсодержитФамилия Имя Отчество, если все три поля заполнены у пользователя добавочного;- если ФИО заполнено не полностью,
fromсодержит номер телефона; toсодержит внешний номер назначения без знака+.
5.4. Объект записи
Если запись не создавалась, поле recording равно null.
Возможные значения recording.status:
CAPTUREDACCEPTEDUPLOADEDAVAILABLEFAILEDARCHIVED
Скачивание разрешено только при:
{
"available": true
}
Значение available=true используется для статусов AVAILABLE и ARCHIVED.
При остальных статусах downloadUrl равен null.
6. Пагинация
Результаты сортируются от новых к старым по времени начала звонка.
Если meta.hasMore=true, передайте meta.nextCursor без изменений:
curl -sS \
-H "X-Api-Key: $FOURPHONE_API_KEY" \
"https://api.4phone.uz/api/v1/external/calls?limit=50&cursor=ЗНАЧЕНИЕ_ИЗ_NEXT_CURSOR"
Cursor привязан к:
- тенанту;
- отделу ключа;
- направлению;
- статусам;
- периоду;
- телефону;
- добавочному;
- фильтру записи.
Нельзя менять фильтры между страницами. При изменении фильтра начните запрос
без cursor. Измененный или поврежденный cursor вернет 400.
Для регулярной синхронизации используйте период с небольшим перекрытием и
удаляйте дубликаты по id. Это защищает от пропуска звонка при временной
ошибке сети.
7. Карточка звонка
GET /calls/{id}
Требуемый scope: calls:read.
curl -sS \
-H "X-Api-Key: $FOURPHONE_API_KEY" \
"https://api.4phone.uz/api/v1/external/calls/$CALL_ID"
Ответ содержит тот же объект, что элемент массива data.
API возвращает 404, если:
- звонок не существует;
- звонок принадлежит другому тенанту;
- звонок находится в недоступном отделе;
- звонок внутренний;
- звонок еще не получил завершенный статус.
Одинаковый ответ 404 не раскрывает наличие чужих данных.
8. Получение записи
HEAD /calls/{id}/recording
Проверяет доступность и метаданные без передачи файла.
curl -sS -I \
-H "X-Api-Key: $FOURPHONE_API_KEY" \
"https://api.4phone.uz/api/v1/external/calls/$CALL_ID/recording"
GET /calls/{id}/recording
Скачивает файл.
curl -fS \
-H "X-Api-Key: $FOURPHONE_API_KEY" \
-o call-recording.wav \
"https://api.4phone.uz/api/v1/external/calls/$CALL_ID/recording"
Требуемый scope для обоих методов: recordings:read.
8.1. Каналы WAV
Если запись двухканальная:
- файл остается в формате WAV;
- первый канал содержит входящий поток исходной стороны вызова;
- второй канал содержит исходящий поток в сторону исходной стороны вызова;
- для входящего звонка это обычно клиент в первом канале и оператор во втором;
- для исходящего звонка это обычно оператор в первом канале и клиент во втором;
- запись продолжает работать при очередях и переводах вызова.
Старые записи могут оставаться одноканальными. Интеграция должна читать число каналов из заголовка WAV, а не определять его по дате или имени файла.
Метод GET передает исходный файл без перекодирования и сведения каналов, а
HEAD возвращает его метаданные. URL, scope recordings:read, Range-запросы и
остальные правила API для двухканальных записей не меняются. Объект recording
в JSON не содержит число каналов, оно определяется из заголовка скачанного WAV.
Пример проверки файла:
ffprobe -v error \
-select_streams a:0 \
-show_entries stream=codec_name,sample_rate,channels,channel_layout \
-of default=noprint_wrappers=1 \
call-recording.wav
Двухканальный WAV занимает примерно вдвое больше места, чем одноканальная запись той же длительности.
Ответ содержит:
Content-Typeс форматом файла;Content-Dispositionс безопасным именем;Content-Length, если размер известен;Accept-Ranges: bytes;Cache-Control: private, no-store.
Поддерживается частичная загрузка:
curl -fS \
-H "X-Api-Key: $FOURPHONE_API_KEY" \
-H "Range: bytes=0-1048575" \
-o first-megabyte.bin \
"https://api.4phone.uz/api/v1/external/calls/$CALL_ID/recording"
Для частичного ответа возвращается HTTP 206 и заголовок Content-Range.
9. Webhooks звонков
REST API подходит для выгрузки и сверки. Для получения звонков в реальном времени настройте webhook.
9.1. Создание
- Откройте
Настройки. - Перейдите в
Интеграции. - Откройте
Webhooks. - Нажмите
Добавить webhook. - Укажите название.
- Укажите публичный HTTPS URL клиентского обработчика.
- Выберите
CALL_COMPLETED. - Выберите
CALL_RECORDING_READY, если нужна запись. - Оставьте секрет пустым для безопасной автоматической генерации.
- Сохраните показанный секрет в secret manager клиентской системы.
Webhook URL должен:
- использовать HTTPS;
- не содержать логин и пароль;
- иметь публичное DNS имя или публичный IP;
- разрешаться только в публичные IP адреса;
- быть доступен из интернета.
Локальные, loopback, link-local и приватные сети блокируются для защиты от SSRF. Политика URL проверяется при сохранении и перед каждой доставкой.
Секрет должен иметь длину от 16 до 256 символов. В базе он хранится в зашифрованном виде. После создания секрет повторно не показывается.
Если секрет потерян, нажмите Сменить секрет. После ротации клиентский
обработчик нужно сразу переключить на новое значение.
9.2. CALL_COMPLETED
Событие создается после завершения обработки CDR для входящего или исходящего звонка. Внутренние звонки исключаются.
Если запись еще обрабатывается, событие уже содержит звонок, но:
{
"recording": {
"available": false,
"downloadUrl": null
}
}
9.3. CALL_RECORDING_READY
Событие создается отдельно, когда файл получает статус AVAILABLE или
ARCHIVED. Проверка ожидающих записей выполняется каждые 30 секунд.
Событие содержит тот же data.id, что CALL_COMPLETED, и:
{
"recording": {
"available": true,
"downloadUrl": "/api/v1/external/calls/847cc73c-6aaf-4a5e-aea8-d106fae10c45/recording"
}
}
Если обработка записи завершилась статусом FAILED,
CALL_RECORDING_READY не отправляется.
9.4. Формат webhook
POST /your/4phone/webhook
Content-Type: application/json
X-Webhook-Id: 8f4a3b2c-1234-4567-89ab-1234567890ab
X-Webhook-Timestamp: 1784770859
X-Webhook-Event: CALL_COMPLETED
X-Webhook-Signature: v1,abcdef1234567890
{
"id": "8f4a3b2c-1234-4567-89ab-1234567890ab",
"event": "CALL_COMPLETED",
"data": {
"schemaVersion": 1,
"id": "847cc73c-6aaf-4a5e-aea8-d106fae10c45",
"callId": "17ed2d85-a64a-4ce0-9151-a4dc57505dc6",
"direction": "INBOUND",
"status": "ANSWERED",
"date": "23.07.2026 10:40:59",
"dateIso": "2026-07-23T05:40:59Z",
"timeZone": "Asia/Tashkent",
"from": "998901234567",
"to": "781139788",
"durationSeconds": 83,
"extensionNumber": "101",
"recording": {
"status": "CAPTURED",
"available": false,
"fileName": "17ed2d85-a64a-4ce0-9151-a4dc57505dc6.wav",
"contentType": "audio/wav",
"sizeBytes": null,
"downloadUrl": null
}
},
"sentAt": "2026-07-23T05:40:59.000Z",
"timestamp": 1784770859
}
id в корне является ID webhook сообщения. data.id является ID звонка.
9.5. Проверка HMAC подписи
Подпись вычисляется от исходных байтов HTTP body:
signedContent = X-Webhook-Id + "." + X-Webhook-Timestamp + "." + rawBody
signature = "v1," + HMAC_SHA256_HEX(secret, signedContent)
Важно использовать исходный body до JSON разбора. Повторная сериализация JSON может изменить байты и привести к несовпадению подписи.
Пример для Node.js и Express:
import crypto from "node:crypto";
import express from "express";
const app = express();
const secret = process.env.FOURPHONE_WEBHOOK_SECRET;
app.post(
"/4phone/webhook",
express.raw({ type: "application/json" }),
(request, response) => {
const messageId = String(request.header("X-Webhook-Id") || "");
const timestamp = String(request.header("X-Webhook-Timestamp") || "");
const received = String(request.header("X-Webhook-Signature") || "");
const rawBody = request.body;
const signedContent = Buffer.concat([
Buffer.from(`${messageId}.${timestamp}.`),
rawBody,
]);
const expected =
"v1," +
crypto.createHmac("sha256", secret).update(signedContent).digest("hex");
const valid =
received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!valid || !Number.isFinite(age) || age > 300) {
return response.status(401).json({ accepted: false });
}
const payload = JSON.parse(rawBody.toString("utf8"));
// Сначала проверьте уникальность payload.id в своей базе.
// Затем поставьте обработку в локальную очередь.
return response.status(202).json({ accepted: true });
},
);
Та же формула проверки применяется в Python и PHP: важно передать в HMAC исходные байты HTTP body без повторной сериализации JSON.
9.6. Идемпотентность
4phone использует доставку как минимум один раз. Клиент обязан корректно обрабатывать повторное сообщение.
Сохраняйте корневой id или X-Webhook-Id в таблице обработанных сообщений.
Если такой ID уже существует, верните успешный HTTP ответ без повторного
выполнения бизнес операции.
Для звонков также используйте data.id как уникальный ключ записи. События
CALL_COMPLETED и CALL_RECORDING_READY имеют разные webhook ID, но один
data.id.
9.7. Ответ и повторные попытки
Успешной считается доставка, при которой endpoint вернул HTTP статус от 200 до 299.
Требования к обработчику:
- проверить подпись;
- проверить timestamp;
- проверить идемпотентность;
- быстро сохранить событие или поставить его в локальную очередь;
- ответить в течение 10 секунд.
При сетевой ошибке, timeout или неуспешном HTTP статусе выполняется до 8 попыток. Интервалы после ошибок: 30, 60, 120, 240, 480, 960 и 1920 секунд.
Для каждой попытки обновляются timestamp, sentAt и HMAC подпись.
X-Webhook-Id остается прежним.
10. История доставок
В разделе Настройки, Интеграции, История доставок отображаются:
- тип события;
- целевой webhook;
- статус;
- последний HTTP код;
- количество попыток;
- последняя ошибка;
- время каждой из последних попыток.
Статусы:
PENDINGожидает отправки;PROCESSINGотправляется сейчас;DELIVEREDуспешно принят клиентом;FAILEDожидает следующей попытки;DLQисчерпал автоматические попытки.
Для FAILED и DLQ доступен ручной Replay.
11. Rate limit
Лимиты API ключа считаются в Redis отдельно для каждого метода в окне 60 секунд:
- до 300 запросов к обычному методу;
- до 120 запросов к GET или HEAD записи.
Ответ содержит:
X-RateLimit-Limit
X-RateLimit-Remaining
X-RateLimit-Reset
При превышении возвращается 429 и Retry-After: 60.
На внешнем Nginx дополнительно действует общий лимит API. Клиент должен
обрабатывать 429, учитывать Retry-After и использовать backoff.
12. Ошибки
Ошибка возвращается в JSON:
{
"statusCode": 403,
"timestamp": "2026-07-30T12:41:59.676Z",
"path": "/api/v1/external/calls?limit=1",
"code": "api_key.scope_missing",
"message": "Missing API key scopes: calls:read"
}
Основные HTTP статусы и коды:
400 calls.invalid_date_rangeозначает неверный диапазон дат;400 calls.invalid_phoneозначает отсутствие цифр в фильтре телефона;400 calls.invalid_cursorозначает поврежденный cursor;400 calls.cursor_filter_mismatchозначает изменение фильтров пагинации;401 api_key.requiredозначает отсутствие ключа;401 api_key.invalidозначает неверный или отозванный ключ;401 api_key.expiredозначает истекший ключ;403 api_key.ip_not_allowedозначает несовпадение IP whitelist;403 api_key.scope_missingозначает отсутствие нужного scope;404 calls.not_foundозначает недоступный звонок;404 recordings.not_foundозначает отсутствие или ошибку записи;409 recordings.not_readyозначает, что файл еще обрабатывается;416 recordings.invalid_rangeозначает неверный байтовый диапазон;429 rate_limit.exceededозначает превышение лимита ключа.
13. Пример регулярной синхронизации
Пример на Node.js:
const baseUrl = "https://api.4phone.uz/api/v1/external";
const apiKey = process.env.FOURPHONE_API_KEY;
async function request(path) {
const response = await fetch(`${baseUrl}${path}`, {
headers: {
"X-Api-Key": apiKey,
},
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(
`4phone API ${response.status}: ${error.code || error.message || "unknown"}`,
);
}
return response.json();
}
async function loadCalls(startedFrom) {
const calls = [];
let cursor = null;
do {
const query = new URLSearchParams({
startedFrom,
limit: "100",
});
if (cursor) {
query.set("cursor", cursor);
}
const page = await request(`/calls?${query.toString()}`);
calls.push(...page.data);
cursor = page.meta.nextCursor;
} while (cursor);
return calls;
}
Сохраняйте id как уникальное поле. После успешного цикла обновляйте
checkpoint по dateIso, оставляя небольшое временное перекрытие при следующем
запросе.
14. Диагностика
API возвращает 401
Проверьте:
- ключ передан в
X-Api-Keyили Bearer заголовке; - значение скопировано полностью;
- ключ не отозван;
- срок действия не истек.
API возвращает 403
Проверьте:
- выбран scope
calls:read; - для записи выбран scope
recordings:read; - IP whitelist содержит публичный исходящий IP клиента.
Список пустой
Проверьте:
- период и часовую зону в
startedFromиstartedTo; - направление и статусы;
- ограничение ключа по отделу;
- значение
extensionNumber; - что звонок не является внутренним.
Запись возвращает 409
Файл еще обрабатывается. Дождитесь CALL_RECORDING_READY или повторите
проверку с backoff.
Webhook не создается
Проверьте:
- webhook активен;
- выбрано нужное событие;
- URL использует HTTPS;
- DNS возвращает только публичные IP;
- секрет существует.
Webhook имеет FAILED или DLQ
Откройте историю доставок и проверьте:
- HTTP код;
- текст последней ошибки;
- время ответа;
- доступность TLS сертификата;
- что endpoint отвечает быстрее 10 секунд.
После исправления используйте Replay.
HMAC подпись не совпадает
Проверьте:
- используется исходный raw body;
- секрет относится к этому webhook;
- строка содержит ID, timestamp и body в правильном порядке;
- перед hex значением есть префикс
v1,; - серверное время синхронизировано;
- после ротации используется новый секрет.
15. Безопасность
- Создавайте отдельный ключ для каждой системы.
- Выдавайте только необходимые scopes.
- Ограничивайте production ключ публичным IP клиента.
- Устанавливайте срок действия тестовым ключам.
- Не храните ключи и webhook секреты в Git.
- Не передавайте секреты в URL.
- Не логируйте заголовки авторизации.
- Проверяйте HMAC до JSON обработки.
- Проверяйте окно timestamp не более 5 минут.
- Обеспечивайте идемпотентность по webhook ID.
- При утечке сразу отзывайте ключ или ротируйте webhook секрет.
16. Как это работает внутри 4phone
- FreeSWITCH завершает звонок и отправляет данные CDR в backend.
- Backend сохраняет CDR с тенантом, направлением, номерами, временем, добавочным и состоянием записи.
- Post-call обработчик формирует нормализованный объект API.
- Для входящих и исходящих звонков создается
CALL_COMPLETED. - Запись поступает через NFS и при необходимости архивируется в MinIO.
- После доступности файла создается
CALL_RECORDING_READY. - Событие сохраняется в durable delivery хранилище и отправляется через очередь.
- API ключ всегда ограничивает запрос тенантом, scopes, отделом и IP.
- Файл отдается потоком без раскрытия внутреннего пути хранилища.
Основные части реализации:
backend/src/modules/external-api/external-calls.controller.tsbackend/src/modules/external-api/services/external-calls.service.tsbackend/src/modules/calls/external-call-payload.service.tsbackend/src/modules/api-keys/api-keys.service.tsbackend/src/modules/calls/post-call.service.tsbackend/src/modules/deliveries/deliveries.service.tsbackend/src/modules/recordings/recordings.service.ts
17. Совместимость
Текущая версия payload:
{
"schemaVersion": 1
}
Новые необязательные поля могут добавляться без смены URL API. Клиент не должен отклонять payload из-за неизвестного поля. Ломающие изменения должны публиковаться в новой версии API.