Звонки
Журнал звонков компании (GET /calls, GET /calls/counters) и программное управление голосовыми звонками: инициировать исходящий вызов к контакту (POST /calls/originate) и завершить активный вызов (POST /calls/{call}/hangup). Это API управления звонками, а не браузерный софтфон — оно лишь ставит вызов в работу через телефонный шлюз AISAR; сам звук идёт по SIP/WebRTC вне HTTP. Также включает «Пульт супервайзера» (/v1/telephony/*): активные звонки, presence команды, wallboard, историческую статистику, очередь перезвона по пропущенным и действия супервайзера над живым звонком (listen/whisper/barge/transfer). И часть редактора схем маршрутизации звонков: список схем с фильтром по направлению, превью линии дозвона для оператора (GET /telephony/outbound-line) и назначение операторов/команд на исходящую схему. А также полный цикл управления схемами: создание, редактирование, граф узлов (builder), привязка каналов к входящей схеме и библиотека аудио-приглашений.
Доступ и требования
Вся группа доступна только при активной подписке и включённом на плане функционале телефонии (telephony); кроме того, вызывающий пользователь должен быть заведён как оператор телефонии (иметь провиженный SIP-эндпоинт) и обладать правом telephony.use в текущей компании — иначе 403 { "data": { "message": "Telephony is not available for your account." } }.
С 2026-09-08 из журнала звонков и его счётчиков исключены звонки контактов, скрытых из рабочего inbox (POST /v1/contacts/{contact}/hide-from-inbox) — в том числе для супервизоров с правом company_telephony.manage; уведомление о пропущенном звонке по такому контакту тоже не отправляется. С 2026-09-29 то же правило действует и по прямому id: GET /calls/{call}/recording-url (в том числе via=download) и GET /calls/{call}/transcript (расшифровка и сводка) для звонка скрытого контакта отвечают 404 — как для звонка другой компании, до проверки участника и права company_telephony.manage, то есть одинаково для любого пользователя. Уже выданные ссылки на запись такого звонка тоже перестают проигрываться (404), а в галереях вложений диалога и контакта запись не показывается. Ничего не удаляется: после возврата контакта в inbox запись и расшифровка снова доступны.
С 2026-09-26 явно выбранная линия (channel_id), закрытая для оператора правилами доступа к каналам (channel_access), отклоняется так же, как несуществующая: POST /calls/originate — 404 «Contact or channel not found.», POST /softphone/external-call и POST /contacts/{contact}/call-permission/request, GET /contacts/{contact}/outbound-eligibility — 404 «Channel not found.». Автоматический выбор линии по-прежнему делает сервер.
Инициировать вызов (`POST /calls/originate`)
POST /calls/originate принимает contact_id (обязателен, целое) и channel_id (с 2026-09-14 необязателен — исходящие схемы маршрутизации звонков, см. группу Телефония ниже). Канал задаёт способ дозвона: канал WhatsApp/WABA звонит контакту в WhatsApp, а канал типа sip_telephony набирает телефонный номер контакта через SIP-транк. Если channel_id передан, он используется, когда это разрешено схемой оператора и политикой компании (подробности и коды отказа — в разделе «Выбор номера: три ступени» ниже) — и контакт, и канал должны принадлежать текущей компании, иначе 404 { "data": { "message": "Contact or channel not found." } }. Если channel_id не передан, линию выбирает тот же резолвер, что отвечает на GET /telephony/outbound-line: у вызывающего оператора есть исходящий маршрут — звонок уходит по нему; маршрута нет — звонок уходит так же, как до появления этой функции. Маршрут запретил звонок или не нашёл ни одной доступной линии → 422 { "data": { "message": "...", "block_reason": string|null } } (в первом случае) или 422 { "data": { "message": "No line available for this call." } } (во втором). Параметр device (необязателен, с 2026-09-22: browser | native) задаёт, на какое устройство оператора придёт операторская нога: browser — браузерный софтфон / встраиваемый iframe (единственное поведение до этой даты), native — нативный SIP-профиль оператора (agent-{id}: SIP-приложение партнёра, Zoiper, настольный телефон). Без device нога идёт в браузер, если у оператора есть зарегистрированная вкладка, иначе — на нативный профиль, если зарегистрирован он. INVITE операторской ноги несёт заголовки X-AISAR-Call-Id (= call.id из ответа) и X-AISAR-Leg: outbound, по которым SIP-клиент отличает эхо собственного звонка от входящего.
Ответ и статусы звонка
Успех (200) возвращает созданную запись звонка: { "data": { "call": { id, direction: "outbound", status: "ringing", to_number, from_number, asterisk_channel_id, timeline_message_id } } } — вызов создаётся в состоянии ringing, а в таймлайн диалога добавляется карточка звонка (timeline_message_id). Дальнейшие переходы статуса (ringing → answered → completed, либо rejected / missed / failed) приходят через события реального времени и вебхуки семейства call.*, а не в теле этого ответа.
Разрешение на звонок (WhatsApp и SIP)
WhatsApp-звонки требуют действующего разрешения на звонок (call permission) от контакта: без него originate вернёт 422 с блоком eligibility, где call.can_call = false. Проверить готовность и запросить разрешение заранее можно через GET /v1/contacts/{contact}/outbound-eligibility и POST /v1/contacts/{contact}/call-permission/request (группа «Контакты»). Для SIP-каналов разрешение не требуется (eligibility.call.permission_status = "not_required"), нужен лишь телефонный номер у контакта — иначе 422 { "data": { "message": "Contact has no phone number to dial." } }.
Прочие исходы originate
Прочие исходы originate:
403с блокомeligibility— канал или контакт не поддерживает звонок (нет WhatsApp-аккаунта, оператор не включён).422 { "data": { "message": "No registered native SIP device for this operator." } }— запрошенdevice: "native", а нативное устройство оператора не зарегистрировано; клиента при этом никто не набирает.502— телефонный шлюз не смог инициировать вызов.422— ошибка валидации тела (отсутствуютcontact_id/channel_id).- Канал с
sip_outbound_gated: true(см. группу «Каналы») пропускается при выборе SIP-канала для звонка — если это единственный SIP-канал компании,originateведёт себя так, как будто подключённого SIP-канала нет вовсе. - Только для
sip_telephony-канала в режиме подключения по IP-адресу: дополнительная защита от злоупотреблений — запрещённый префикс номера или превышение лимита в 300 исходящих звонков в час на канал — возвращает422с сообщением"This destination cannot be dialled from this line."либо"Too many calls placed on this line recently. Try again later.". Каналы, подключённые по логину/паролю, этой проверке не подвергаются.
Завершение вызова (`POST /calls/{call}/hangup`)
POST /calls/{call}/hangup завершает активный вызов. Помимо доступности телефонии, завершить вызов может только его участник (назначенный оператор либо сторона вызова) или пользователь с правом управления телефонией компании — иначе 403. Успех: { "message": "Hangup requested." } (запрос на разрыв передан в шлюз; фактическое завершение подтверждается событием call.*). Обратите внимание: ответ hangup, в отличие от originate, не обёрнут в data.
Расшифровка звонка (`GET /calls/{call}/transcript`)
GET /calls/{call}/transcript отдаёт расшифровку и сводку голосовой ИИ-сессии (ElevenLabs), привязанной к звонку. У эндпоинта СВОЙ гейт доступа, отдельный от общего требования группы — telephony.use и провиженный SIP-аккаунт здесь не обязательны. Достаточно любого из трёх условий: (а) вызывающий — участник звонка (назначенный оператор либо сторона вызова, та же проверка, что у hangup); (б) у вызывающего есть право company_telephony.manage; (в) звонок принял ИИ-агент (assignee_type = "ai_agent") и у вызывающего есть conversation.view в диалоге этого звонка, причём с 2026-09-25 сам диалог должен быть ему виден по правилам доступа к каналам (channel_access команд; иначе — 403) — так расшифровку может открыть любой оператор, читающий этот диалог, а не только супервайзер. Для звонка, принятого человеком, conversation.view доступа не даёт: такую расшифровку видит только сам участник или менеджер телефонии.
Компания сверяется первой, до любой другой проверки: чужой либо несуществующий {call} — 404 (как будто записи не существует), а не 403. Если компания совпала, но ни одна из трёх веток доступа не выполнена — 403. Если у звонка вообще нет голосовой сессии (обычный звонок без ИИ-агента) — тоже 404: { "data": { "message": "No voice session for this call." } }.
Успешный ответ (200): { "data": { voiceCallId, callId, status, outcome, summary, transcript: [...], startedAt, answeredAt, endedAt, timezone, durationSec } } — обратите внимание, поля этого ответа в camelCase, а не в snake_case, как в остальной группе. voiceCallId — UUID голосовой сессии, callId — id звонка (совпадает с {call} из пути). status — состояние сессии (например dialing/in_progress/completed/failed). outcome — resolved/failed/null, если вендор не определил исход. summary — текстовая сводка звонка либо null. startedAt/answeredAt/endedAt — ISO-8601 либо null. timezone — IANA-часовой пояс компании, в котором эти метки времени нужно показывать пользователю. durationSec — длительность разговора в секундах либо null; она берётся у самого звонка, а не из тарифицируемых секунд голосовой сессии, поэтому совпадает с duration_sec той же строки журнала GET /calls.
Каждый элемент transcript[] — реплика: role (строка, user/agent/…), text, at_sec (секунда звонка, число) и tool_calls[] — вызовы инструментов ИИ-агента на этой реплике. Каждый элемент tool_calls[] отдаёт фиксированный набор полей: type, tool_name, request_id, params_as_json (строка — аргументы вызова как прислал вендор), tool_has_been_called (bool), result (результат инструмента: разобранный JSON, сырая строка или null, если результат не зафиксирован), is_error (bool либо null; null значит «результат не зафиксирован», а НЕ успех), error (текст ошибки вендора либо null) и latency_sec (число секунд либо null). Вендор может прислать вызов инструмента и его результат на разных репликах — API уже сопоставляет их по request_id и отдаёт результат на той же реплике, где случился сам вызов.
Ссылка на запись звонка (`GET /calls/{call}/recording-url`)
GET /calls/{call}/recording-url минтит одноразовую подписанную ссылку на аудиозапись звонка — точечный аналог поля recording_url в журнале (GET /calls), только выпущенный по явному клику, а не заранее для каждой строки списка. Гейт доступа полностью идентичен transcript (см. выше): участник звонка, company_telephony.manage или читатель диалога ИИ-звонка через conversation.view; тот же порядок 404 (чужая компания) → 403 (нет права). Если для звонка нет сохранённой записи (recording_status не stored, либо запись не удалось сопоставить) — 404 { "data": { "message": "No recording for this call." } }.
Успешный ответ (200): { "data": { "recording_url": "<подписанная ссылка>" } } — ссылка живёт 15 минут (короче, чем 30-минутная ссылка из списка журнала, так как минтится по прямому клику «слушать/скачать», а не заранее). Необязательный query-параметр via=play|download (по умолчанию play) — download добавляет Content-Disposition: attachment, заставляя браузер скачать файл вместо проигрывания. Лимит — 30 запросов в минуту на пользователя.
Журнал звонков (`GET /calls`)
GET /calls возвращает журнал звонков компании в конверте { "data": { "items": [...], "pagination": {...} } } (конвенция листинговых эндпоинтов этого API, НЕ конверт { "data": [...] }). По умолчанию оператор видит только звонки, к которым он причастен (назначенный, звонящий или принимающий сторона); право company_telephony.manage расширяет видимость до всех звонков компании — это одно и то же ограничение видимости, что используется в GET /calls/counters, так что счётчики никогда не разойдутся со списком. Базовые фильтры (все необязательные query-параметры): filter=all|missed (по умолчанию all; missed — непринятые входящие звонки), direction=inbound|outbound, channel_id (целое, должен существовать), q (строка до 100 символов — регистронезависимый поиск подстроки по from_number/to_number и имени контакта), assignee_type=operator|ai_agent, page (≥1), perPage (1–100, по умолчанию 20). Сортировка — по убыванию started_at. Лимит: 120 запросов в минуту на пользователя (лимитер telephony-calls-index, добавлен вместе с архивными фильтрами ниже — до этого у GET /calls не было отдельного троттлинга); при превышении — 429.
Фильтры архива записей (`GET /calls`, Ф3c)
Тот же GET /calls поддерживает дополнительный набор необязательных query-параметров для режима архива «Записи» — если их не передавать, поведение журнала (в т.ч. используемого во встроенном чате) не меняется: has_recording=1 — только звонки с сохранённой записью (recording_status = "stored"); assigned_user_id (целое) — звонки конкретного оператора (значение не проверяется на существование — id из чужой компании просто не даёт совпадений, а не 404/422, т.к. видимость и так ограничена текущей компанией); date_from/date_to (даты YYYY-MM-DD, date_to >= date_from) — календарные даты, резолвятся в часовом поясе компании и переводятся в UTC-границы суток, как в GET /telephony/stats; with_summary=1 — добавляет в ответ блок summary по ВСЕМУ отфильтрованному набору (см. ниже), а не только по текущей странице; lazy_recordings=1 — не подписывать recording_url заранее для каждой строки списка (поле будет null у всех звонков, включая те, где есть запись) — ссылка выдаётся по клику через GET /calls/{call}/recording-url. Фильтр channel_id (целое) не проверяется на существование: чужой или несуществующий id даёт 200 с пустым data.items, нечисловое значение — 422.
При with_summary=1 ответ дополнительно содержит { "data": { ..., "summary": { "count": N, "total_duration_sec": S } } } — количество звонков и суммарная длительность в секундах по ВСЕМ звонкам, подходящим под текущий набор фильтров (не только по звонкам на странице). Блок summary отсутствует в ответе, если with_summary не передан.
Каждый элемент items[] — объект звонка: id, direction, status (ringing/answered/completed/rejected/missed/failed), provider (whatsapp/sip/internal), from_number, to_number, временные метки started_at/answered_at/ended_at (ISO-8601 либо null), duration_sec, вложенные contact (id, display_name, avatar_url) и assignee (id, name), channel_id/channel_name/channel_type (слаг типа канала, ChannelType::key, например whatsapp_business или sip_telephony), conversation_id, timeline_message_id, recording_status, recording_url, has_recording (bool, = recording_status === "stored") и has_transcript (bool — есть ли у звонка голосовая ИИ-сессия, т.е. вернёт ли GET /calls/{call}/transcript диалог, а не 404). recording_url — временная подписанная ссылка (TTL ~30 минут); присутствует, только когда recording_status = "stored" И запрос НЕ передавал lazy_recordings=1 — при lazy_recordings=1 поле всегда null, даже у звонков с записью (используйте вместо него GET /calls/{call}/recording-url по клику). has_recording, в отличие от recording_url, не зависит от lazy_recordings и остаётся достоверным индикатором наличия записи в обоих режимах. pagination содержит { page, perPage, total, lastPage }.
Фильтры-переходы из статистики (`GET /calls`)
Ещё один набор необязательных query-параметров GET /calls — переходы из графиков статистики и из уведомления о пропущенном звонке. Все они аддитивны: запрос без них ведёт себя ровно как раньше.
status— сырой статус звонка:ringing,answered,completed,rejected,missed,failed,answered_elsewhere. Он строже, чемfilter=missed(тот дополнительно ограничен входящими); если передать оба, применяются оба.hour_from/hour_to(целые 0–23, включительно с обеих сторон) — час НАЧАЛА звонка в локальном часовом поясе компании, тот же, по которому построен рядby_hourв статистике, так что клик по столбцу графика открывает ровно те звонки, из которых он сложен. Можно передать только один из двух.hour_to < hour_from— законный интервал через полночь:hour_from=18&hour_to=2— ночная смена.unassigned=1— звонки без назначенного оператора И без ИИ-агента. Это отдельный булев параметр, а неassigned_user_id=null: в HTTP строка «null» неотличима от контакта, который так и называется.callback_state=pending|resolved— пропущенные входящие, ещё открытые либо уже закрытые в очереди перезвона. Предикат тот же, что уGET /telephony/callbacks(входящий + пропущенный + состояние), поэтому журнал и очередь не разойдутся.has_transcript=1— только звонки, у которых есть что читать (расшифровка сохранена). Обратите внимание: фильтр строже, чем одноимённое полеhas_transcriptв строке ответа — полеtrueуже при наличии голосовой сессии, даже если её расшифровка так и не доехала.assignee_type=operator|ai_agent— кто взял звонок; параметр существовал и раньше, теперь это ещё и переход из строкиby_assignee.
Поиск по расшифровкам (`GET /calls?q_in=`)
Параметр q_in задаёт, где ищется значение q: contact (по умолчанию — номера и имя контакта, прежнее поведение), transcript (текст расшифровок голосовых звонков) или all (и там, и там). Поиск по расшифровкам полнотекстовый, а не подстрочный: слова ищутся целиком, без морфологии; при all два набора совпадений объединяются по идентификаторам звонков. Все остальные фильтры запроса (период, канал, оператор, часы) применяются к поиску тоже — искать всегда ищется внутри уже отфильтрованного журнала.
Поиск по расшифровкам — отдельная, более дорогая ветка, у неё свои ограничения:
- При
q_in=transcript|allпараметрqобязателен и не может быть пустым — иначе422. Пустой запрос по расшифровкам был бы не поиском, а выгрузкой всех разговоров компании. - Свой лимит: 20 запросов в минуту на пользователя (обычный журнал по-прежнему 120/мин); при превышении —
429. - Свой бюджет времени: если поиск не уложился примерно в 5 секунд, он снимается и возвращается
504 { "data": { "message": "Transcript search took too long — try a longer or more specific query." } }. Лечится сужением периода или уточнением запроса. - Токены встраивания (embed) к поиску по расшифровкам не допускаются:
q_in=transcript|allс таким токеном —403. Сам журнал, архив записей и поиск по умолчанию (q_in=contact) остаются встроенному чату доступны как прежде. - Каждая ветка поиска отдаёт не более 500 совпадений; если лимит выбран, ответ скажет об этом через
meta.search_truncated.
При q_in=transcript|all ответ дополнен блоком meta — { "search_truncated": bool, "search_cap": 500 } (у обычного журнала блока meta нет). У тех строк items[], где совпадение нашлось в расшифровке, появляется transcript_matches[] — до трёх коротких цитат: at_sec (секунда звонка, на которую нужно перемотать плеер, число), role (кто говорит), text (обычный текст, без HTML-разметки; длинная реплика обрезается) и highlights — [{ start, end }], смещения найденных слов внутри text в СИМВОЛАХ (не в байтах), end не включается. Подсветку рисует клиент: API специально не отдаёт размеченный HTML, потому что эти цитаты — текст, который произнёс клиент, и он уходит в мобильное приложение, во встроенные CRM-окна и в вебхуки. У строк без совпадения ключа transcript_matches просто нет.
Счётчики звонков (`GET /calls/counters`)
GET /calls/counters возвращает бейдж-счётчики { "data": { "all": N, "missed": M } } — построены из того же самого запроса видимости, что и GET /calls (без фильтров direction/channel_id), поэтому число никогда не разойдётся со списком. Параметров нет.
Пульт супервайзера: доступ
Все эндпоинты /v1/telephony/* ниже (пульт, статистика, очередь перезвона, действия супервайзера) гейтятся ОТДЕЛЬНО от остальной группы: не проверкой telephony-фичи тарифа и telephony.use (это гейт /calls/* выше), а правом company_telephony.manage — без него 403. У вызывающего не обязателен провиженный SIP-эндпоинт (в отличие от telephony.use). На момент публикации отдельного права telephony.supervise не существует: доступ идёт через право company_telephony.manage, которым по умолчанию обладает роль admin (продуктовое решение 2026-08-13).
Пульт: активные звонки, presence, wallboard
Три read-only эндпоинта без параметров, каждый отдаёт снимок «прямо сейчас» (не пагинированы). GET /telephony/console/active-calls — { "data": { "items": [...] } }, каждый элемент: id, direction, kind, provider, status, contact (id/display_name/avatar_url либо null), channel_id, channel_name, from_number, to_number, started_at/answered_at (ISO-8601 либо null), waiting_sec (время в звонке до ответа, либо время звонка на текущий момент, если ещё звонит), talk_sec (текущая длительность разговора, null пока не отвечен), assignee ({ type: "operator"|"ai_agent"|null, id, name }).
GET /telephony/console/presence — { "data": { "items": [...] } }, по одному элементу на оператора с активным SIP-аккаунтом: user_id, name, avatar_url (фото оператора: внешняя ссылка как есть, загруженный файл — подписанной ссылкой, либо null, если фото нет), availability (сырое значение доступности пользователя), status — вычисленный статус: on_call (в разговоре), acw (после звонка, короткое окно донастройки), offline, away, free; devices ({ browser: { registered } | null, external: { registered } | null }) и metrics_today ({ answered, missed, avg_talk_sec }, в часовом поясе компании). GET /telephony/console/wallboard — плоский объект { "data": { active_calls, ringing, operators_online, operators_free, missed_today, service_level_today, service_level_threshold_sec, lines } }; operators_online/operators_free считаются из того же presence, поэтому никогда не разойдутся с ним. service_level_today — готовый процент (0–100), не доля, а service_level_threshold_sec — эхо порога сервис-левела из конфигурации (тот же, что в GET /telephony/stats), чтобы подписать KPI «в пределах N сек», не дублируя значение у себя.
wallboard.lines — состояние телефонных линий компании, то есть каналов, на которых прямо сейчас можно принимать и делать звонки (тот же признак, что calling_enabled в группе «Каналы», архивные каналы исключены). Массив, по элементу на линию: channel_id, name, provider_key (sip_telephony, whatsapp_business, whatsapp), number (config.calling_did, иначе имя аккаунта канала), state (up — линия работает, connecting — подключается, down — недоступна, unknown — состояние неизвестно), degraded (линия считается рабочей, но сигнал хуже нормы — например, SIP-транк в IP-режиме не отвечает на OPTIONS), registration_status (сырой статус регистрации SIP-транка либо null у не-SIP линий), checked_at, last_call_at (ISO-8601 либо null) и last_call_answered_by ({ id, name } — оператор, ответивший на последний звонок линии; null, если звонок был пропущен, ещё идёт или его принял ИИ-агент). Если у компании нет ни одной линии, массив пустой.
Статистика (`GET /telephony/stats`)
GET /telephony/stats — исторические KPI и ряды для графиков за период. Обязательный query-параметр period=today|7d|30d|custom; для custom дополнительно обязательны date_from/date_to (даты, date_to >= date_from, диапазон не более 92 дней — иначе 422). Периоды считаются в часовом поясе компании, затем переводятся в UTC. Ответ — плоский объект { "data": { from, to, timezone, total, answered, missed, missed_not_called_back, avg_talk_sec, service_level, service_level_threshold_sec, ai_share, directions, waiting_buckets, by_day, by_hour, by_channel, by_assignee } }: timezone — IANA-часовой пояс компании, в котором посчитаны границы периода и ряды by_day/by_hour (подписывайте даты и часы в нём, а не в часовом поясе браузера); service_level и ai_share — готовые проценты (0–100, не доли); service_level_threshold_sec — эхо порога сервис-левела из конфигурации; missed_not_called_back (целое) — сколько пропущенных за период всё ещё открыто в очереди перезвона (тот же предикат, что у GET /telephony/callbacks при state=pending), поэтому это число никогда не больше missed; directions — { inbound, outbound }, разбивка периода по направлению (считаются все звонки направления, отвеченные и нет, поэтому inbound + outbound === total); waiting_buckets — распределение времени ожидания клиентов (см. ниже); by_day — [{ date, answered, missed }]; by_hour — [{ hour, total, answered, missed }]; by_channel — [{ channel_id, channel_name, provider, total }]; by_assignee — [{ type, id, name, answered, missed, avg_talk_sec }]. Необязательный channel_ids[] (массив целых, до 50) ограничивает все показатели номерами компании из списка; без параметра считаются все номера. Чужой или несуществующий id даёт нули (200), не ошибку.
Про by_hour: ряд всегда содержит все 24 часа (пропущенные часы заполнены нулями), час — локальный час компании. total считает звонки ОБОИХ направлений, а answered/missed — только входящие: неотвеченный исходящий звонок не является провалом обслуживания. Отсюда два следствия, которые нельзя нарушать при отрисовке: answered + missed НЕ равно total (в часе с исходящим трафиком или со звонком, который ещё звонит либо завершился ошибкой, сумма меньше) — складывайте answered/missed в стек и подписывайте столбец значением total. И sum(by_hour[].answered) не сходится с by_day[].answered (дневной ряд и KPI answered считают все направления) — сверять почасовой ряд с дневным не нужно.
Про waiting_buckets — { median_sec, buckets }, сколько клиенты ждали ответа. Считается ТОЛЬКО по отвеченным входящим звонкам, ожидание = answered_at − started_at. Ожидание на пропущенном звонке — это терпение звонившего, а не скорость ответа, поэтому две величины не смешиваются: потери отдаются отдельно (missed, by_hour[].missed, строка «никто не взял» в by_assignee), а у сорванных звонков своё поле waited_sec в очереди перезвона. buckets — всегда ровно 4 элемента { from_sec, to_sec, count }, даже за пустой период (тогда все count: 0, median_sec: null), у последнего to_sec: null (верх открыт). Границы привязаны к порогу сервис-левела SL (service_level_threshold_sec): 0–10, 10–SL, SL–2×SL, 2×SL и выше. При SL по умолчанию (20 секунд) это 0–10 / 10–20 / 20–40 / 40+, а при более жёстком пороге первая граница опускается вслед за ним (половина SL, но не больше 10 секунд), чтобы четыре интервала оставались строго возрастающими. median_sec — медиана ожидания в секундах либо null, если отвеченных входящих в периоде не было. Сумма count по всем корзинам всегда равна числу отвеченных входящих за период.
В by_assignee последней строкой всегда идёт «никто не взял»: { "type": null, "id": null, "name": null, "answered": 0, "missed": N, "avg_talk_sec": null } — пропущенные входящие за период, которые не приписаны ни одному оператору и ни одному ИИ-агенту (в том числе те, что не предлагались ни одной группе дозвона, чьи операторы получили собственные строки). Строка присутствует всегда, даже когда missed: 0, — её не нужно ни изобретать на стороне клиента, ни считать аномалией. answered в ней структурно 0 (у отвеченного звонка всегда есть исполнитель), а avg_talk_sec — null, а не 0: никто не разговаривал, это не то же самое, что «разговаривал ноль секунд». Оговорка про сумму: звонок, который одновременно звонил двум операторам с собственными строками, засчитан как пропущенный каждому из них, поэтому сумма missed по by_assignee может превышать KPI missed на величину такого пересечения.
Очередь перезвона (`/telephony/callbacks`)
GET /telephony/callbacks (с 2026-09-10) — список пропущенных входящих звонков как задач «кому перезвонить». Очередь не хранится отдельной таблицей, а выводится из журнала звонков, поэтому её видимость совпадает с GET /calls: скрытые из рабочего inbox контакты не попадают ни туда, ни сюда. Строки сгруппированы по собеседнику (сначала по contact_id, а для звонка без контакта — по точному совпадению номера целиком; анонимные звонки не объединяются вовсе), поэтому три пропущенных звонка от одного человека — это одна строка с attempts: 3, а не три задачи.
Параметры: period=today|7d|30d (по умолчанию 7d — пропущенный в пятницу вечером звонок должен пережить выходные), state=pending|resolved|all (по умолчанию pending), page (1–1000) и perPage (1–100, по умолчанию 50); значения вне диапазона — 422. Ответ: { "data": { items, pagination, counters, timezone } }, где pagination — { page, perPage, total, lastPage }, counters — { pending, resolved_today } для бейджа, а timezone — часовой пояс компании, в котором посчитаны периоды и resolved_today. Элемент items[]: call_id, contact ({ id, display_name, avatar_url } либо null), from_number, channel_id, channel_name, conversation_id, missed_at, waited_sec (сколько звонивший ждал ответа до срыва — это не то же самое, что скорость ответа на отвеченном звонке, поэтому две величины никогда не усредняют вместе), attempts (не меньше 1), outside_working_hours (звонок пришёл вне рабочего расписания компании) и resolution — null для открытой задачи, иначе { type, at, by_user, call_id, duration_sec }.
POST /telephony/callbacks/{call}/resolve снимает задачу вручную: тело { "resolution": "called_back" | "dismissed" } (обязательное, другое значение — 422). Успех (200): { "data": { "call_id": N, "resolution": { ... } } }. Вручную закрывать очередь обычно не требуется: когда исходящий звонок тому же собеседнику ОТВЕЧЕН, все его открытые пропущенные закрываются автоматически, в окне 30 дней назад; неудавшийся дозвон очередь не трогает. У автоматического закрытия тот же resolution.type: "called_back", что и у ручного, но заполнены resolution.call_id и resolution.duration_sec — это тот самый исходящий звонок, который закрыл задачу (у ручного закрытия они null). Лимит resolve — 60 запросов в минуту на пользователя, при превышении 429.
403— у вызывающего нет праваcompany_telephony.manage(то же право, что и у остального пульта), либо запрос идёт embed-токеном: оба эндпоинта очереди закрыты для токенов встраиваемых виджетов, потому что очередь — это список имён и телефонов контактов.404(толькоresolve) — звонок не существует, принадлежит другой компании или не виден вызывающему. Проверка права идёт ДО поиска звонка, поэтому по коду ответа нельзя перебирать чужие id звонков.409(толькоresolve) — звонок не является пропущенным входящим либо задача уже закрыта (в том числе автоматически другим оператором); повторный вызов не «переоткрывает» и не переписывает закрытие.
Живое обновление: при каждом изменении очереди (звонок финализирован как пропущенный, задача закрыта вручную или автоматически) на company-канал уходит WebSocket-событие telephony.callback.updated с payload { callId, pending } — этого достаточно, чтобы перерисовать бейдж и сбросить кэш списка, сам список приходит только из REST.
Действия супервайзера (`listen`/`whisper`/`barge`/`transfer`)
Четыре POST /telephony/console/calls/{call}/{action} для живого звонка: listen — тихое прослушивание, whisper — суфлирование оператору (клиент не слышит), barge — включение в разговор (слышно обеим сторонам), transfer — перевод другому оператору. Все 4 действия требуют, чтобы звонок был именно в статусе answered — иначе 422 (изначально transfer допускал ещё звонящий ringing звонок, но такой перевод не поддержан ни бриджем, ни UI пульта, поэтому в ревью это было выровнено с остальными тремя действиями). transfer принимает обязательное тело { "target_user_id": <int> } — получатель должен быть реальным (не сервис-аккаунтом) участником той же компании И иметь там активный SIP-аккаунт оператора, иначе 422. Успех (200): { "data": { "message": "Supervisor action executed.", "action": "<listen|whisper|barge|transfer>" } }; следом асинхронно уходит вебхук call.supervised. Лимит — 30 запросов в минуту на пользователя (лимитер telephony-supervise), при превышении 429.
404— звонок не найден или принадлежит другой компании.403— вызывающий это сервис-аккаунт (PAT), ЛИБО у него нет праваcompany_telephony.manage(проверяется в этом порядке).422— звонок неактивен (уже завершён либо статус неringing/answered), либо ещё не отвечен (answeredтребуется для ВСЕХ 4 действий, включаяtransfer), либо у звонка пуст/отсутствуетasterisk_channel_id, либо (толькоtransfer) не переданtarget_user_id, либо получатель не является активным SIP-оператором именно этой компании, либо бридж отклонил действие по собственной причине — тело ответа отдаёт фиксированное сообщение, сырая ошибка бриджа в него не попадает (логируется на сервере).409— звонок уже супервайзится другим пользователем (гард на стороне бриджа).410— живой канал звонка пропал с бриджа между проверкой и вызовом (звонок завершился в этот момент).502— бридж (asterisk-bridge) недоступен или вернул ошибку ARI.
Исходящие схемы звонков (волна 1)
GET /telephony/schemes возвращает схемы маршрутизации звонков компании и принимает необязательный direction=inbound|outbound (без параметра — все схемы). Элемент списка: id, direction (inbound|outbound), name, comment, status (draft|active), timezone, outbound_priority (целое, по умолчанию 100 — тай-брейк между КОМАНДАМИ, указывающими на разные исходящие схемы: меньшее значение побеждает; для входящей схемы поле не имеет смысла), channels_count, assigned_operators_count, assigned_teams_count (сколько операторов/команд привязано к схеме — вход исходящей схемы; для входящей всегда 0), outbound_line (только у исходящей схемы, и только если её первый узел dial_external называет конкретную линию: { "channel_id", "name", "number" }, иначе null), node_count, updated_at. Требует права company_telephony.manage, лимитер telephony-schemes (120 запросов в минуту на пользователя) — тот же, что у остального редактора схем.
Направление задаётся при создании схемы (POST /telephony/schemes, тело содержит direction) и не меняется впоследствии — входящая и исходящая схемы используют непересекающиеся наборы узлов графа. Основная схема сотрудника выбирает линию ПО УМОЛЧАНИЮ; может ли он вручную выбрать другую, решает политика компании «выбор номера перед звонком» (см. раздел «Выбор номера: три ступени» ниже). Компания без исходящей схемы работает как до этой фичи: GET /telephony/outbound-line вернёт "scheme": null и прежний список кандидатов (первый подходящий канал компании).
GET /telephony/outbound-line — превью того, на какую линию уйдёт следующий звонок оператора: используется тот же резолвер (OutboundLineResolver), что и постановка самого звонка, поэтому подсказка набора номера не может разойтись с реальным звонком. Параметры: to_number (телефон, ^\+?\d{3,20}$) или contact_id — один из двух обязателен, иначе 422. Гейт — право telephony.use (это чтение о СВОЁМ следующем звонке оператора, а не административное действие), отдельный лимитер telephony-outbound-line — 60 запросов в минуту на пользователя (эндпоинт опрашивается на каждое нажатие клавиши в наборе номера). Полная форма ответа: { "scheme": { "id", "name" } | null, "blocked", "block_reason", "candidates": [ { "channel_id", "name", "dial_mode", "is_default" } ], "rejected": [ { "channel_id", "reason" } ] }. candidates упорядочен, линия с is_default: true (всегда первая) предзаполняется в наборе номера; rejected объясняет, почему остальные линии компании отсеяны — channel_not_connected, provider_ip_gated, либо код того же WhatsApp-разрешения на звонок, что и GET /contacts/{contact}/outbound-eligibility (группа «Контакты»). blocked: true означает, что сама схема запрещает этот звонок (ветка hangup в графе) — тогда block_reason несёт текст, заданный при настройке схемы, а candidates пуст.
В ответе GET /telephony/outbound-line (и в полном, и в урезанном виде для embed) есть поле allowed_channel_ids — массив id линий (int[]), которые оператору разрешено выбирать вручную по его исходящей схеме, либо null, если у сотрудника нет исходящей схемы и допустима любая линия. По этому полю интерфейс сужает список «Позвонить через».
Ручной выбор линии подчиняется схеме: если в POST /calls/originate или POST /softphone/external-call передан channel_id, которого нет в схеме оператора, ответ — 422 { "data": { "message": "This line is not allowed by your outbound route.", "error_code": "line_not_in_scheme" } }. Проверка идёт после проверки доступа к каналу (404). Без исходящей схемы ограничения нет.
Выбор номера: три ступени
С 2026-10-20 номер для исходящего звонка выбирается в три ступени. Ступень 1 — ОСНОВНАЯ схема сотрудника: из набора её линий берётся номер по умолчанию, а резервы, нарисованные внутри самой схемы (карусель, цепочка «не дозвонились, пробуем следующий»), работают как раньше. Ступень 2 — общий номер компании (default_channel_id в /companies/current/telephony/outbound-settings) для сотрудников без схем. Ступень 3 — ручной выбор номера перед звонком (channel_id), он зависит от переключателя manual_pick_enabled. Сотрудник может состоять в нескольких исходящих схемах; основную назначает админ (PUT /companies/current/members/{memberId}/telephony/outbound-primary, группа «Пользователи и команды»), а если явного выбора нет, действует прежний порядок: личная схема раньше командной, затем outbound_priority, затем время создания. Звонок НИКОГДА не переходит в другую схему и не подставляет общий номер сам: если основная схема не смогла дозвониться, резервы внутри неё отработали, а дальше звонок не уходит.
Переключатель manual_pick_enabled (по умолчанию true, то есть как до обновления) относится ко ВСЕМ поверхностям, включая API для интеграторов. Если компания его выключила, то channel_id, которого нет среди линий основной схемы сотрудника, отклоняется: POST /calls/originate и POST /softphone/external-call отвечают 422 { "data": { "message": "...", "error_code": "manual_pick_disabled" } }. channel_id, совпадающий с линией основной схемы, проходит — клиенты, которые всегда шлют номер по умолчанию, не ломаются. Это ломающее изменение для интеграторов, которые передают произвольный channel_id: прежде чем компания выключит переключатель, убедитесь, что ваш клиент либо не шлёт channel_id (линию выберет сервер), либо берёт его из GET /telephony/outbound-line (lines[].is_default или primary_scheme). Компании, которые ничего не настраивали, не затронуты: переключатель включён, схем нет, поведение прежнее. Если у сотрудника нет схем и общий номер не задан, переключатель ничего не ограничивает; то же при основной схеме с шагом «любая доступная линия». Если channel_id не передан, а основная схема сотрудника или общий номер недоступны, звонок не уходит: POST /calls/originate отвечает 422 с error_code: "scheme_blocked" и block_reason: "company_default_unavailable" (общий номер отключён, удалён или не может звонить), а превью GET /telephony/outbound-line — blocked: true с тем же block_reason.
Новые и расширенные поля. GET /telephony/outbound-line: manual_pick_enabled (boolean; единственное новое поле и в урезанном виде для embed — по нему интерфейс решает, показывать ли выбор номера), только в полной форме: primary_scheme ({ "id", "name" } или null) и candidates[].source (primary — линия основной схемы, company_default — общий номер, legacy — прежний выбор «любая доступная»). В блоке линий карточки контакта (contact_id) каждая линия получает source (primary | scheme | company_default | legacy) и scheme_name (название схемы или null); для embed-токена scheme_name не отдаётся никогда, source остаётся. Без contact_id (звонилка) ответ дополнительно содержит selectable_lines — линии, которые сотрудник может выбрать вручную: [ { "channel_id", "name", "kind", "from_number", "source", "scheme_name" } ], сначала основная и общий номер, затем линии других его схем; при выключенном manual_pick_enabled — только линия основной схемы. Embed softphone-токен получает этот список без from_number и scheme_name, chat-embed — без scheme_name. GET /telephony/schemes: у исходящих схем добавлены outbound_channel_ids (все линии, на которые схема может набирать, int[]) и assignees ({ "operators": [ { "id", "name" } ], "teams": [ { "id", "name" } ] }, до 5 операторов и до 5 команд — это превью, не полный список; полный даёт GET .../assignments); у входящих схем оба поля null. Эндпоинты, перечисленные выше, гейтятся company_telephony.manage и недоступны embed-токенам, кроме GET /telephony/outbound-line (его урезанный вид для embed описан выше); GET /telephony/schemes для embed-токена теперь отвечает 403, потому что в списке появились имена сотрудников и команд.
GET /companies/current/telephony/outbound-settings и PATCH по тому же пути — политика компании. Ответ: { "data": { "default_channel_id": int|null, "default_channel": { "id", "name", "number" } | null, "default_channel_available": boolean|null, "manual_pick_enabled": boolean } }; default_channel_available равен null, пока общий номер не задан, и false, когда он задан, но недоступен (тогда звонки сотрудников без схем уходят в отказ company_default_unavailable, пока номер не заменят). PATCH принимает любое подмножество default_channel_id (id линии своей компании или null, чтобы снять) и manual_pick_enabled (boolean) и возвращает тот же ответ. Линия, которая не принадлежит компании, удалена, в архиве или не умеет звонить, отклоняется одинаково: 422 { "data": { "error_code": "default_channel_invalid" } }. Гейт — company_telephony.manage; PATCH ещё требует полноправный токен (не ограниченный по правам); embed-токены получают 403. Лимитер telephony-outbound-settings. Каждое изменение пишет аудит telephony.outbound_settings.updated и отправляет в WebSocket событие telephony.outbound.updated (см. руководство по событиям реального времени).
dial_mode у каждого кандидата — dial_direct (SIP-транк или серый/личный WhatsApp: набирается всегда, без окна разрешения и без бюджета Meta) либо dial_window_gated (WhatsApp Business Calling: нужно окно разрешения от абонента и неисчерпанный 24-часовой бюджет звонков). В волне 1 из софтфона (клавиатуры набора) доступны только линии dial_direct.
Для embed-поверхностей (виджет встраиваемого софтфона и чат-iframe партнёрских панелей — токен со способностью embed:softphone:* либо embed:read) ответ ЖЁСТКО урезан по требованию ИБ-ревью 2026-09-13: вместо scheme/candidates/rejected отдаются ровно 4 поля — { "blocked", "block_reason", "default_channel_id", "dial_mode" }, где default_channel_id/dial_mode относятся к тому же кандидату, что candidates[0] в полной форме. Полный перечень линий компании (их DID-номера), название схемы и причины отказа других линий — это инвентарь телефонии компании, который встроенная в чужой origin поверхность видеть не должна; урезание не зависит от того, звонок это или превью.
Кто звонит через исходящую схему — GET/PUT /telephony/schemes/{scheme}/assignments, зеркало привязки каналов к входящей схеме. Гейт — company_telephony.manage, тот же лимитер telephony-schemes. {scheme} — uuid, не скопированный по компании при route-model binding, поэтому оба метода ПЕРВЫМ делом проверяют scheme.company_id и отвечают 404 { "data": { "message": "Scheme not found." } } на чужую или несуществующую схему — раньше проверки тела запроса. GET отвечает { "data": { "assignments": { "operators": [ { "id", "name" } ], "teams": [ { "id", "name" } ], "conflicts": [ { "user_id", "via_team_id", "personal_scheme_id" } ] } } }; мягко удалённые операторы/команды в список не попадают. conflicts — устаревшее поле, с 2026-10-20 всегда пустой массив [] (оставлено ради совместимости): сотрудник может состоять в нескольких исходящих схемах, личное назначение больше не «перебивает» командное.
PUT /telephony/schemes/{scheme}/assignments принимает ПОЛНЫЙ желаемый набор, не дельту: { "operator_ids": [...], "team_ids": [...] } (оба поля обязательны как массивы, пустой массив — законный способ сказать «никто»; лимиты — 500 операторов и 200 команд). Всё, что не перечислено, отвязывается. И операторы, и команды должны реально состоять в компании (роль в model_has_roles для операторов, teams.company_id для команд) — иначе 422 со списком чужих id в errors.operator_ids/errors.team_ids. С 2026-10-20 запрос НИКОГО не переносит: оператор или команда могут состоять в нескольких исходящих схемах сразу (раньше привязка к новой схеме молча снимала её с прежней), поэтому привязка к другой схеме ничего не отнимает у остальных. Поля moved и conflicts в ответе остаются ради совместимости, но всегда пусты: { "data": { "assignments": {...}, "moved": { "operators": [], "teams": [] } } }. Если у сотрудника убрали привязку к его основной схеме, указатель основной схемы сбрасывается сам. 422 { "data": { "errors": { "scheme": [...] } } } — попытка назначить операторов/команды входящей схеме (direction ≠ outbound). 409 — гонка двух одновременных сохранений одного и того же набора (уникальный индекс поймал коллизию первым); нужно повторить запрос. Каждое успешное сохранение пишет запись аудита telephony.outbound_scheme.assigned.
Схемы звонков: создание, граф, привязка
Ниже: всё, что нужно, чтобы собрать маршрутизацию входящих (и исходящих) звонков через API, без кабинета. Доступны только Business-компаниям (restrict.company.type:business) с активной подпиской; каждый вызов требует права company_telephony.manage в текущей компании. Работает cookie-сессия и Bearer-токен компании, в том числе api_token сервисного аккаунта партнёра; embed-токены и токены connect-сессии не допускаются. Лимитер telephony-schemes (120 запросов в минуту на пользователя) общий для всего редактора. Чужая или несуществующая схема отвечает 404 { "data": { "message": "Scheme not found." } }. Ошибки валидации приходят как 422 { "data": { "message", "errors" } }.
Схема. POST /telephony/schemes принимает name (обязателен, до 120 символов), direction (inbound по умолчанию | outbound; после создания НЕ меняется, попытка передать direction в PATCH даёт 422 — создайте новую схему), timezone (идентификатор IANA, например Asia/Almaty; по умолчанию UTC) и comment (до 1000 символов). Ответ 201 { "data": { "scheme": {...} } }: схема создаётся в статусе draft с единственным узлом entry (для исходящей: outbound_entry). Черновик не обрабатывает звонки. GET /telephony/schemes/{scheme} возвращает ту же форму: id, direction, name, comment, timezone, status, outbound_priority, moh_as_ringback, moh_prompt_id, channels[] (id, name), nodes[] (id, type, position {x,y}, data) и edges[] (id, source, target, sourceHandle, label, data).
PATCH /telephony/schemes/{scheme} принимает частичное тело: name, comment, timezone, status (active включает схему, draft выключает), outbound_priority (0..1000, имеет смысл только у исходящей), moh_as_ringback (bool) и moh_prompt_id (id готового аудио-приглашения своей компании или null). Ответ тот же { "data": { "scheme": {...} } }. Активация исходящей схемы, чей вход никуда не ведёт, отклоняется: 422, errors.status: ["route_leads_nowhere"]. DELETE /telephony/schemes/{scheme} отвечает { "data": { "message": "Scheme deleted." } }.
Граф схемы: PATCH /telephony/schemes/{scheme}/builder
PATCH /telephony/schemes/{scheme}/builder ПОЛНОСТЬЮ заменяет граф схемы: старые узлы и рёбра удаляются, записываются присланные (не дельта; сначала прочитайте GET, правьте и отправляйте целиком). Тело: nodes[] (обязателен, минимум 1; каждый: id строка до 255 символов, type, необязательные position {x,y} и data-объект) и edges[] (source, target: клиентские id узлов; sourceHandle; необязательные id, label, data). Клиентские id узлов связывают рёбра внутри запроса; id существующего узла этой схемы (uuid из GET) сохраняется, остальные получают новые uuid от сервера (итоговые id вернёт ответ — { "data": { "scheme": {...} } }), а ребро с неизвестным source/target молча отбрасывается. Подробности про сохранение узлов и потолки размера — в следующем абзаце. Ровно один входной узел: entry у входящей схемы, outbound_entry у исходящей, иначе 422 (errors.nodes).
Сохранение графа сохраняет идентификаторы узлов: узел, чей id совпадает с узлом этой же схемы, обновляется на месте, а не пересоздаётся, поэтому звонки, которые прямо сейчас стоят в меню, очереди или на звонке группе, не обрываются при правке схемы. Чужой, неизвестный или не-uuid id создаёт новый узел. Узлы, которых нет в теле запроса, удаляются; рёбра всегда записываются заново. Потолки размера графа (превышение даёт 422): не больше 200 узлов, 400 рёбер; data одного узла до 16 КБ, data ребра до 2 КБ, весь граф (узлы и рёбра) до 256 КБ. data.timeout_sec узлов ring_group и responsible_manager должен быть целым от 5 до 600, иначе 422 на nodes.N.data.timeout_sec.
Типы узлов зависят от направления; чужой тип даёт 422 на nodes.N.type. Входящая схема: entry, schedule, ring_group, hangup, client_check, play, voicemail, responsible_manager, ivr_menu, queue, ai_agent. Исходящая: outbound_entry, schedule, dial_external, hangup. sourceHandle ребра — одно из out, work, off, no_answer, known, new, no_input, timeout, digit_0..digit_9, escalated, failed; какие из них у узла бывают — в списке ниже. Ключи data не проверяются схемой запроса (узел с неизвестными ключами сохранится как есть), поэтому пользуйтесь только перечисленными.
entry: безdata; единственная веткаoutк первому шагу.play:data.prompt_id(id готового аудио-приглашения); веткаout. Приглашение не задано или ещё не готово: узел пропускается, звонок идёт поout.ivr_menu:data.prompt_id(меню),data.digits[](какие цифры принимаются, строками, например["1","2"]),data.timeout_sec(ожидание нажатия, 2..30, по умолчанию 5),data.repeats(сколько раз проиграть меню, 1..5, по умолчанию 2). Ветки:digit_Nдля каждой принятой цифры иno_input(тишина, неверная цифра после исчерпания повторов, нет готового приглашения). Принятая цифра без ребраdigit_Nобрывает звонок.schedule: либоdata.intervals[]из{ "weekday": 1..7, "start": "09:00", "end": "18:00" }(weekdayпо ISO, 1 = понедельник; времяHH:MMв часовом поясе СХЕМЫ;endне позжеstartозначает переход через полночь, но засчитывается только вечерняя часть интервала), либоdata.use_company_hours: true(рабочие часы компании, те же, что у автоответа чата; компания без расписания считается всегда открытой). Веткиworkиoff; обе должны быть соединены, иначе звонок обрывается. Работает и в исходящей схеме.ring_group:data.members={ "operators": [user_id], "teams": [team_id] }(пусто — вся компания),data.strategy(all— звонить всем сразу, по умолчанию;sequential— по очереди;priority— группами изdata.priority_groups[], каждая{ "users": [user_id] }),data.timeout_sec(сколько звонить на шаге, 5..600, по умолчанию 25),data.record(bool, по умолчаниюtrue; запись реально включается только если запись разговоров включена в компании),data.record_stereo(bool),data.ringback(standard|scheme_moh| id приглашения). Веткаno_answer(никто не ответил или некому звонить). Если вdata.membersотмечены команды или операторы, но их не осталось (команда удалена, пуста или чужая), узел не звонит всей компании, а сразу уходит вno_answer; пустой выбор (ни команд, ни операторов) звонит всей компании.voicemail:data.greeting_id(приглашение-приветствие, необязательно),data.max_sec(длина сообщения, 10..600, по умолчанию 120); веткаoutпосле записи. Голосовая почта не считается записью разговора и не отключается настройкой записи.hangup:data.reason(строка-причина для журнала, по умолчаниюscheme_hangup); без веток.- Остальные узлы:
client_check(веткиknown/new: звонящий найден среди контактов или нет),responsible_manager(звонок ответственному за контакт; веткаno_answer),queue(очередь с ожиданием; веткаtimeout),ai_agent(голосовой ИИ-сотрудник; веткиescalated/failed; доступен не всем компаниям); исходящиеoutbound_entryиdial_external(линия или карусель линий:data.channel_id/data.carousel.channel_ids[], до 20, линии должны быть вашей компании и уметь звонить). Подробные ключи этих узлов здесь не фиксируются.
Мёртвая ветка — нормальная ошибка маршрута, а не ошибка запроса: PATCH .../builder её не проверяет, но во время звонка узел без нужного ребра (например schedule без off) завершает вызов. Поэтому соединяйте все ветки узла. Для узла ai_agent с включённой эскалацией ребро escalated обязательно уже при сохранении.
Привязка каналов к входящей схеме
Входящая схема срабатывает на звонки по тем каналам, к которым она привязана, и только пока status равен active. GET /telephony/scheme-channels возвращает { "data": { "channels": [...] } }: каналы компании, способные принимать звонки (SIP, WABA с включёнными звонками, личный WhatsApp в режиме приёма звонков); элемент: id, name, type, calling_did, country, status, has_scheme, scheme_id, scheme_name, scheme_status. POST /telephony/schemes/{scheme}/channels принимает { "channel_ids": [int] } — ПОЛНЫЙ набор каналов схемы (поле обязательно, пустой массив отвязывает все; каналы вне набора отвязываются); все каналы должны быть вашей компании, иначе 422 (errors.channel_ids). POST /telephony/channels/{channel}/scheme принимает { "scheme_id": "uuid" | null } (поле обязательно; null отвязывает) и привязывает ОДИН канал; чужой канал — 404 Channel not found.. Исходящие схемы к каналам не привязываются (422, Outbound schemes are not bound to channels.): они включаются через назначение операторов (PUT /telephony/schemes/{scheme}/assignments). Оба POST отвечают { "data": { "message": "..." } }.
Аудио-приглашения
Приветствия, меню IVR и музыка ожидания берутся из библиотеки приглашений компании. GET /telephony/audio-prompts возвращает { "data": { "items": [ { "id", "name", "status", "original_filename", "created_at" } ] } }. POST /telephony/audio-prompts — multipart/form-data с полями name (до 120 символов) и file (mp3, wav, ogg, oga, m4a, aac, webm или mp4, до 15 МБ). Файл перекодируется под телефонию и доставляется на телефонный сервер синхронно: успех — 201 { "data": { "item": {...} } } со status: "ready"; если перекодировать или доставить не удалось — 422 с message. Проигрывается только приглашение в статусе ready; узлы play/ivr_menu/voicemail со ссылкой на неготовое или чужое приглашение пропускаются. DELETE /telephony/audio-prompts/{prompt} — { "data": { "message": "Prompt deleted." } } (чужое — 404). Идентификатор из id и подставляется в prompt_id, greeting_id, moh_prompt_id.
Команды для ring_group (members.teams) создаются через /v1/teams — см. группу «Команды и пользователи».
Настройки записи разговоров и срок хранения
Три эндпоинта подраздела «Что записываем» (Телефония → Настройки): GET и PATCH /v1/companies/current/telephony/recording-settings, плюс GET /v1/companies/current/telephony/recording-settings/retention-preview. Компания берётся из current_company_id вызывающего — идентификатора компании в пути нет вовсе, поэтому адресовать чужую компанию нечем. Гейт — существующее право company_telephony.manage (у роли admin любой компании оно есть всегда); нового права фича не вводит. Запрос с embed-токеном (способность embed:softphone:* или embed:read) отклоняется сразу, до всякой проверки тела: 403 { "data": { "message": "This action is unauthorized." } }. Лимитер telephony-recording-settings — 30 запросов в минуту на пользователя на все три эндпоинта, при превышении 429. GET и PATCH работают и при неактивной подписке (это выключатель, которым арендатор ОСТАНАВЛИВАЕТ запись и удаление, — терять его вместе с подпиской нельзя); retention-preview требует активной подписки, потому что это лишь вспомогательный расчёт.
GET .../recording-settings отвечает { "data": { "record_calls_enabled": bool, "retention_months": int|null, "retention_options": [3, 6, 12], "retention_changed_at": iso|null, "retention_effective_at": iso|null, "retention_paused_reason": string|null, "last_changed_at": iso|null, "last_changed_by": { "user_id": int|null, "name": string|null } | null, "archive": { "bytes": int|null, "calls_count": int|null, "measured_at": iso|null } } }. retention_months равное null — это значение, а не пропуск: «не удалять», и это умолчание для всех компаний, существующих и новых; удаление по сроку включается только явным выбором. retention_options — белый список сроков в месяцах, который одновременно предлагает интерфейс и принимает PATCH. retention_effective_at — когда ночная уборка ВПЕРВЫЕ применит текущий срок (конец недели остывания, а не ближайшая ночь); null, пока удалять нечего. retention_paused_reason объясняет, почему при выбранном сроке ничего не удаляется: cooling_off, subscription_inactive, company_blocked, company_deleted, invalid_term, missing_change_timestamp, company_missing — либо null, если уборка отработает на следующем прогоне. archive — снимок ночного замера, а не счёт на лету: пока measured_at равен null, объём показывать нечего. Строка настроек создаётся лениво при первом же GET, но ответ всегда 200 (не 201), чтобы форма контракта не зависела от истории компании.
PATCH .../recording-settings принимает частичное тело — трогаются только присланные поля, поэтому клиент, знающий об одном переключателе, не может обнулить настройку, о которой не слышал. Поля: record_calls_enabled (bool), retention_months (null либо одно из значений retention_options; произвольное число — 422 с обычной ошибкой валидации), плюс два флага подтверждения confirm_retention_shortening и acknowledge_recording_notice (bool). Успешный ответ — та же форма, что у GET.
Два условных отказа PATCH отвечают ПЛОСКИМ телом { "message": ..., "error_code": ... } со статусом 422 — без обёртки data, в отличие от обычной ошибки валидации ({ "data": { "message", "errors" } }): фронт различает их по машинному коду и открывает нужный диалог.
recording_notice_acknowledgement_required— переходrecord_calls_enabledизfalseвtrueбезacknowledge_recording_notice: true. Платформа не проигрывает автоматического предупреждения о записи: приветствие маршрута — это загруженный компанией аудиофайл, а у исходящих звонков приветствия нет вовсе. Поэтому включение записи требует признать, что уведомить абонента — обязанность компании-клиента; признание пишется в журнал действий вместе с версией текста. Выключение записи подтверждения не требует.retention_shortening_requires_confirmation— сокращение срока безconfirm_retention_shortening: true. Сокращением считается переход к более короткому сроку (12 → 3) И переход из «не удалять» в любой конкретный срок (null → 12) — последнее и есть самое большое удаление, которое эта настройка может запланировать. Удлинение (3 → 12), возврат к «не удалять» (12 → null) и повторный выбор того же срока подтверждения не требуют, поэтому повторная отправка той же формы идемпотентна.
Побочные эффекты успешного PATCH: любая смена срока (в том числе удлинение) перезапускает неделю остывания — retention_changed_at обновляется, и до конца окна ночная уборка компанию не трогает; включение и выключение записи, а также включение и сокращение срока рассылают уведомление в кабинете ВСЕМ администраторам компании (единственный компенсирующий контроль против угнанной сессии — отдельного права на уничтожение архива фича не вводит); каждое изменение пишет запись аудита (telephony.recording_settings.updated, при включении записи ещё и telephony.recording.consent_acknowledged) и запоминается на строке настроек как last_changed_by/last_changed_at. Удаление по сроку необратимо: ночной прогон удаляет файл записи, кэшированный m4a-дериват, вложение в карточке звонка и стирает расшифровку и сводку связанного голосового звонка, оставляя звонку recording_status: "expired". Прогон ограничен потолком записей на компанию за ночь, поэтому большой архив уменьшается за несколько ночей, а не одним махом.
GET .../recording-settings/retention-preview?months=3 — что именно уничтожит выбранный срок. months обязателен и должен быть из того же белого списка (3, 6, 12), иначе 422: «превью по умолчанию» не существует, потому что умолчание (null, не удалять) не удаляет ничего. Ответ: { "data": { "months": int, "affected_calls": int, "affected_bytes": int, "unmeasured_calls": int, "scrubbed_transcripts": int, "oldest_started_at": iso|null, "first_deletion_at": iso } }. Считается тем же предикатом, что и сама ночная уборка, поэтому диалог описывает именно то удаление, которое санкционирует. unmeasured_calls больше нуля означает, что affected_bytes ЗАНИЖЕН (у части записей размер ещё не измерен) — в таком случае показывают количество записей, а не объём. scrubbed_transcripts — расшифровки, которые будут стёрты вместе с записями; расшифровка звонка, у которого записи не сохранилось, под срок хранения не попадает никогда, поэтому у компании с выключенной записью это число всегда ноль.
Эндпоинты
| Метод | Путь | Описание |
|---|---|---|
| GET | /v1/callsЖурнал звонков компании (пагинация, фильтры all/missed/direction/channel_id/q/assignee_type; архив: has_recording/assigned_user_id/date_from-date_to/with_summary/lazy_recordings). | Журнал звонков компании (пагинация, фильтры all/missed/direction/channel_id/q/assignee_type; архив: has_recording/assigned_user_id/date_from-date_to/with_summary/lazy_recordings). |
| GET | /v1/calls/countersСчётчики звонков `{all, missed}` — та же видимость, что и журнал. | Счётчики звонков `{all, missed}` — та же видимость, что и журнал. |
| POST | /v1/calls/originateИнициировать исходящий звонок контакту через WhatsApp- или SIP-канал; `channel_id` необязателен — без него линию выбирает исходящий маршрут оператора. | Инициировать исходящий звонок контакту через WhatsApp- или SIP-канал; `channel_id` необязателен — без него линию выбирает исходящий маршрут оператора. |
| POST | /v1/calls/{call}/hangupЗавершить активный звонок (доступно участнику вызова или менеджеру телефонии). | Завершить активный звонок (доступно участнику вызова или менеджеру телефонии). |
| GET | /v1/calls/{call}/transcriptРасшифровка и сводка голосового ИИ-звонка (участнику звонка, менеджеру телефонии или читателю диалога ИИ-звонка). | Расшифровка и сводка голосового ИИ-звонка (участнику звонка, менеджеру телефонии или читателю диалога ИИ-звонка). |
| GET | /v1/calls/{call}/recording-urlПодписанная ссылка на запись звонка на 15 минут (тот же гейт, что у transcript). | Подписанная ссылка на запись звонка на 15 минут (тот же гейт, что у transcript). |
| GET | /v1/telephony/console/active-callsПульт супервайзера: список активных звонков компании прямо сейчас. | Пульт супервайзера: список активных звонков компании прямо сейчас. |
| GET | /v1/telephony/console/presenceПульт супервайзера: присутствие/статус операторов телефонии. | Пульт супервайзера: присутствие/статус операторов телефонии. |
| GET | /v1/telephony/console/wallboardПульт супервайзера: сводные KPI «прямо сейчас» (wallboard). | Пульт супервайзера: сводные KPI «прямо сейчас» (wallboard). |
| GET | /v1/telephony/statsИсторическая статистика звонков за период (today/7d/30d/custom) + ряды для графиков. | Историческая статистика звонков за период (today/7d/30d/custom) + ряды для графиков. |
| GET | /v1/telephony/callbacksОчередь перезвона: пропущенные входящие, сгруппированные по собеседнику (period/state/page/perPage) + счётчики. | Очередь перезвона: пропущенные входящие, сгруппированные по собеседнику (period/state/page/perPage) + счётчики. |
| POST | /v1/telephony/callbacks/{call}/resolveСнять задачу из очереди перезвона: `{ "resolution": "called_back" | "dismissed" }`. | Снять задачу из очереди перезвона: `{ "resolution": "called_back" | "dismissed" }`. |
| POST | /v1/telephony/console/calls/{call}/listenСупервайзер: тихо прослушать отвеченный звонок. | Супервайзер: тихо прослушать отвеченный звонок. |
| POST | /v1/telephony/console/calls/{call}/whisperСупервайзер: суфлировать оператору на отвеченном звонке (клиент не слышит). | Супервайзер: суфлировать оператору на отвеченном звонке (клиент не слышит). |
| POST | /v1/telephony/console/calls/{call}/bargeСупервайзер: включиться в отвеченный звонок (слышно обеим сторонам). | Супервайзер: включиться в отвеченный звонок (слышно обеим сторонам). |
| POST | /v1/telephony/console/calls/{call}/transferСупервайзер: перевести звонок другому оператору компании (target_user_id). | Супервайзер: перевести звонок другому оператору компании (target_user_id). |
| GET | /v1/telephony/schemesСписок схем маршрутизации звонков (входящих/исходящих); опциональный фильтр `direction=inbound|outbound`. | Список схем маршрутизации звонков (входящих/исходящих); опциональный фильтр `direction=inbound|outbound`. |
| GET | /v1/telephony/outbound-lineПревью линии для следующего исходящего звонка оператора (`to_number` или `contact_id`) — тот же резолвер, что применит сам звонок. | Превью линии для следующего исходящего звонка оператора (`to_number` или `contact_id`) — тот же резолвер, что применит сам звонок. |
| GET | /v1/telephony/schemes/{scheme}/assignmentsКто (операторы/команды) назначен на исходящую схему; поле `conflicts` с 2026-10-20 всегда пустое. | Кто (операторы/команды) назначен на исходящую схему; поле `conflicts` с 2026-10-20 всегда пустое. |
| PUT | /v1/telephony/schemes/{scheme}/assignmentsЗадать полный набор операторов/команд исходящей схемы (не дельта) — отсутствующие в списке отвязываются; других схем запрос не затрагивает (`moved` всегда пуст). | Задать полный набор операторов/команд исходящей схемы (не дельта) — отсутствующие в списке отвязываются; других схем запрос не затрагивает (`moved` всегда пуст). |
| GET | /v1/companies/current/telephony/outbound-settingsПолитика исходящих компании: общий номер (`default_channel_id`) и переключатель «выбор номера перед звонком» (`manual_pick_enabled`). | Политика исходящих компании: общий номер (`default_channel_id`) и переключатель «выбор номера перед звонком» (`manual_pick_enabled`). |
| PATCH | /v1/companies/current/telephony/outbound-settingsИзменить общий номер и/или переключатель выбора номера (любое подмножество полей); чужая или неработающая линия — `422 default_channel_invalid`. | Изменить общий номер и/или переключатель выбора номера (любое подмножество полей); чужая или неработающая линия — `422 default_channel_invalid`. |
| POST | /v1/telephony/schemesСоздать схему (`name`, `direction` inbound|outbound, `timezone`, `comment`): черновик с одним входным узлом. | Создать схему (`name`, `direction` inbound|outbound, `timezone`, `comment`): черновик с одним входным узлом. |
| GET | /v1/telephony/schemes/{scheme}Схема целиком (builder): настройки, привязанные каналы, узлы и рёбра. | Схема целиком (builder): настройки, привязанные каналы, узлы и рёбра. |
| PATCH | /v1/telephony/schemes/{scheme}Изменить настройки схемы: `name`, `comment`, `timezone`, `status` (`active` включает), `outbound_priority`, `moh_as_ringback`, `moh_prompt_id`. `direction` не меняется. | Изменить настройки схемы: `name`, `comment`, `timezone`, `status` (`active` включает), `outbound_priority`, `moh_as_ringback`, `moh_prompt_id`. `direction` не меняется. |
| DELETE | /v1/telephony/schemes/{scheme}Удалить схему. | Удалить схему. |
| PATCH | /v1/telephony/schemes/{scheme}/builderПолностью заменить граф схемы: `nodes[]` и `edges[]` (ровно один входной узел, типы узлов по направлению). | Полностью заменить граф схемы: `nodes[]` и `edges[]` (ровно один входной узел, типы узлов по направлению). |
| POST | /v1/telephony/schemes/{scheme}/channelsЗадать полный набор каналов входящей схемы (`channel_ids[]`); отсутствующие отвязываются. | Задать полный набор каналов входящей схемы (`channel_ids[]`); отсутствующие отвязываются. |
| POST | /v1/telephony/channels/{channel}/schemeПривязать один канал к входящей схеме (`scheme_id`) или отвязать (`null`). | Привязать один канал к входящей схеме (`scheme_id`) или отвязать (`null`). |
| GET | /v1/telephony/scheme-channelsКаналы компании, способные принимать звонки, с их текущей схемой. | Каналы компании, способные принимать звонки, с их текущей схемой. |
| GET | /v1/telephony/audio-promptsБиблиотека аудио-приглашений компании (`id`, `name`, `status`). | Библиотека аудио-приглашений компании (`id`, `name`, `status`). |
| POST | /v1/telephony/audio-promptsЗагрузить приглашение (`multipart`: `name`, `file` до 15 МБ); играется в статусе `ready`. | Загрузить приглашение (`multipart`: `name`, `file` до 15 МБ); играется в статусе `ready`. |
| DELETE | /v1/telephony/audio-prompts/{prompt}Удалить аудио-приглашение. | Удалить аудио-приглашение. |
| GET | /v1/companies/current/telephony/recording-settingsНастройки записи разговоров компании: включена ли запись, срок хранения (`null` = не удалять), причина паузы уборки и снимок объёма архива. | Настройки записи разговоров компании: включена ли запись, срок хранения (`null` = не удалять), причина паузы уборки и снимок объёма архива. |
| PATCH | /v1/companies/current/telephony/recording-settingsИзменить настройки записи (частичное тело): переключатель записи и срок хранения; включение записи и сокращение срока требуют флага подтверждения. | Изменить настройки записи (частичное тело): переключатель записи и срок хранения; включение записи и сокращение срока требуют флага подтверждения. |
| GET | /v1/companies/current/telephony/recording-settings/retention-previewСколько записей, байт и расшифровок уничтожит выбранный срок (`months` из белого списка) и когда произойдёт первое удаление. | Сколько записей, байт и расшифровок уничтожит выбранный срок (`months` из белого списка) и когда произойдёт первое удаление. |
Примеры
Исходящий WhatsApp-звонок
Запрос
curl -X POST https://api.aisar.app/v1/calls/originate \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"contact_id": 42,
"channel_id": 7
}'Ответ
{
"data": {
"call": {
"id": 5001,
"direction": "outbound",
"status": "ringing",
"to_number": "7700000XXXX",
"from_number": "7708550XXXX",
"asterisk_channel_id": "1720787000.4521",
"timeline_message_id": 88214
}
}
}Исходящий звонок через SIP-транк
Запрос
curl -X POST https://api.aisar.app/v1/calls/originate \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"contact_id": 42,
"channel_id": 12
}'Ответ
{
"data": {
"call": {
"id": 5002,
"direction": "outbound",
"status": "ringing",
"to_number": "7700000XXXX",
"from_number": "7727000XXXX",
"asterisk_channel_id": "1720787100.4530",
"timeline_message_id": 88221
}
}
}Звонок без channel_id — линию выбирает исходящий маршрут
Запрос
curl -X POST https://api.aisar.app/v1/calls/originate \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"contact_id": 42
}'Ответ
{
"data": {
"call": {
"id": 5003,
"direction": "outbound",
"status": "ringing",
"to_number": "7700000XXXX",
"from_number": "7727000XXXX",
"asterisk_channel_id": "1720787200.4540",
"timeline_message_id": 88230
}
}
}Звонок без channel_id, запрещённый маршрутом (422)
Запрос
curl -X POST https://api.aisar.app/v1/calls/originate \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"contact_id": 42
}'Ответ
{
"data": {
"message": "This call is not allowed by your outbound route.",
"block_reason": "international_not_allowed",
"eligibility": null
}
}WhatsApp-звонок без разрешения контакта (422)
Запрос
curl -X POST https://api.aisar.app/v1/calls/originate \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"contact_id": 42,
"channel_id": 7
}'Ответ
{
"data": {
"message": "No valid call permission or daily attempts exhausted.",
"eligibility": {
"message": {
"window_open": true,
"window_expires_at": 1720800000,
"requires_template": false
},
"call": {
"channel_kind": "whatsapp",
"can_call": false,
"permission_status": "none",
"permission_expires_at": null,
"can_request_permission": true,
"request_cooldown_until": null,
"attempts_remaining": 5,
"has_whatsapp_account": true
}
}
}
}Завершение активного звонка
Запрос
curl -X POST https://api.aisar.app/v1/calls/5001/hangup \
-H "Authorization: Bearer YOUR_API_TOKEN"Ответ
{
"message": "Hangup requested."
}Телефония недоступна для аккаунта (403)
Запрос
curl -X POST https://api.aisar.app/v1/calls/originate \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"contact_id": 42,
"channel_id": 7
}'Ответ
{
"data": {
"message": "Telephony is not available for your account."
}
}Журнал звонков
Запрос
curl "https://api.aisar.app/v1/calls?filter=missed&perPage=20" \
-H "Authorization: Bearer YOUR_API_TOKEN"Ответ
{
"data": {
"items": [
{
"id": 5001,
"direction": "inbound",
"status": "missed",
"provider": "whatsapp",
"from_number": "7700000XXXX",
"to_number": "7708550XXXX",
"started_at": "2026-07-11T09:12:00+00:00",
"answered_at": null,
"ended_at": "2026-07-11T09:12:24+00:00",
"duration_sec": 0,
"contact": {
"id": 42,
"display_name": "Aigerim K.",
"avatar_url": null
},
"channel_id": 7,
"channel_name": "WhatsApp — Sales",
"channel_type": "whatsapp_business",
"conversation_id": 913,
"timeline_message_id": 88214,
"recording_status": null,
"recording_url": null,
"has_recording": false,
"has_transcript": false,
"assignee": null
}
],
"pagination": {
"page": 1,
"perPage": 20,
"total": 1,
"lastPage": 1
}
}
}Архив записей: фильтр + сводка + без предподписи
Запрос
curl "https://api.aisar.app/v1/calls?has_recording=1&date_from=2026-08-01&date_to=2026-08-19&with_summary=1&lazy_recordings=1&perPage=20" \
-H "Authorization: Bearer YOUR_API_TOKEN"Ответ
{
"data": {
"items": [
{
"id": 5010,
"direction": "inbound",
"status": "completed",
"provider": "sip",
"from_number": "7727000XXXX",
"to_number": "7008550XXXX",
"started_at": "2026-08-19T09:14:00+00:00",
"answered_at": "2026-08-19T09:14:01+00:00",
"ended_at": "2026-08-19T09:14:52+00:00",
"duration_sec": 51,
"contact": {
"id": 118,
"display_name": "Nurlan T.",
"avatar_url": null
},
"channel_id": 12,
"channel_name": "SIP — Support",
"channel_type": "sip_telephony",
"conversation_id": 921,
"timeline_message_id": 88250,
"recording_status": "stored",
"recording_url": null,
"has_recording": true,
"has_transcript": true,
"assignee": {
"id": 9,
"name": "Данияр О."
}
}
],
"pagination": {
"page": 1,
"perPage": 20,
"total": 1,
"lastPage": 1
},
"summary": {
"count": 1,
"total_duration_sec": 51
}
}
}Счётчики звонков
Запрос
curl https://api.aisar.app/v1/calls/counters \
-H "Authorization: Bearer YOUR_API_TOKEN"Ответ
{
"data": {
"all": 128,
"missed": 6
}
}Расшифровка звонка (ИИ перевёл на оператора)
Запрос
curl https://api.aisar.app/v1/calls/5010/transcript \
-H "Authorization: Bearer YOUR_API_TOKEN"Ответ
{
"data": {
"voiceCallId": "01927e6a-2b3f-7c91-8e0a-4f6b8b1d9a02",
"callId": 5010,
"status": "completed",
"outcome": "resolved",
"summary": "Клиент попросил соединить с оператором для уточнения деталей заказа.",
"transcript": [
{
"role": "user",
"text": "Переключите на человека",
"at_sec": 40,
"tool_calls": []
},
{
"role": "agent",
"text": "",
"at_sec": 47,
"tool_calls": [
{
"type": "webhook",
"tool_name": "voice_transfer_to_human",
"request_id": "toolu_8f2a1c9d4e",
"params_as_json": "{\"reason\":\"client asked\"}",
"tool_has_been_called": true,
"result": { "ok": true, "message": "Escalated." },
"is_error": false,
"error": null,
"latency_sec": 0.979
}
]
},
{
"role": "agent",
"text": "Соединяю со специалистом.",
"at_sec": 49,
"tool_calls": []
}
],
"startedAt": "2026-08-19T09:14:00+00:00",
"answeredAt": "2026-08-19T09:14:01+00:00",
"endedAt": "2026-08-19T09:14:52+00:00",
"timezone": "Asia/Almaty",
"durationSec": 51
}
}У звонка нет голосовой сессии (404)
Запрос
curl https://api.aisar.app/v1/calls/5002/transcript \
-H "Authorization: Bearer YOUR_API_TOKEN"Ответ
{
"data": {
"message": "No voice session for this call."
}
}Ссылка на запись звонка
Запрос
curl "https://api.aisar.app/v1/calls/5010/recording-url?via=download" \
-H "Authorization: Bearer YOUR_API_TOKEN"Ответ
{
"data": {
"recording_url": "https://api.aisar.app/v1/messages/88250/attachments/4021/media?company=3&disposition=attachment&expires=1755651600&signature=8f2a1c9d4e6b7f0a3c5d8e1f2a4b6c9d0e3f5a7b8c1d2e4f6a9b0c3d5e7f8a1"
}
}Для звонка нет записи (404)
Запрос
curl https://api.aisar.app/v1/calls/5002/recording-url \
-H "Authorization: Bearer YOUR_API_TOKEN"Ответ
{
"data": {
"message": "No recording for this call."
}
}Очередь перезвона
Запрос
curl -G "https://api.aisar.app/v1/telephony/callbacks" \
--data-urlencode "period=7d" \
--data-urlencode "state=pending" \
--data-urlencode "perPage=20" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Ответ
{
"data": {
"items": [
{
"call_id": 5121,
"contact": {
"id": 812,
"display_name": "Алия Сериковна",
"avatar_url": null
},
"from_number": "+12135550111",
"channel_id": 41,
"channel_name": "SIP — основной номер",
"conversation_id": null,
"missed_at": "2026-09-09T19:42:10+00:00",
"waited_sec": 27,
"attempts": 3,
"outside_working_hours": true,
"resolution": null
},
{
"call_id": 5118,
"contact": null,
"from_number": "+12135550166",
"channel_id": 41,
"channel_name": "SIP — основной номер",
"conversation_id": null,
"missed_at": "2026-09-09T11:05:02+00:00",
"waited_sec": 12,
"attempts": 1,
"outside_working_hours": false,
"resolution": null
}
],
"pagination": { "page": 1, "perPage": 20, "total": 2, "lastPage": 1 },
"counters": { "pending": 2, "resolved_today": 5 },
"timezone": "Asia/Almaty"
}
}Снять задачу из очереди перезвона
Запрос
curl -X POST "https://api.aisar.app/v1/telephony/callbacks/5121/resolve" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "resolution": "called_back" }'Ответ
{
"data": {
"call_id": 5121,
"resolution": {
"type": "called_back",
"at": "2026-09-10T08:15:44+00:00",
"by_user": { "id": 55, "name": "Асем" },
"call_id": null,
"duration_sec": null
}
}
}Задача уже закрыта (409)
Запрос
curl -X POST "https://api.aisar.app/v1/telephony/callbacks/5118/resolve" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "resolution": "dismissed" }'Ответ
{
"data": {
"message": "Callback already resolved."
}
}Статистика звонков за 7 дней
Запрос
curl -G "https://api.aisar.app/v1/telephony/stats" \
--data-urlencode "period=7d" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Ответ
{
"data": {
"from": "2026-09-03T19:00:00+00:00",
"to": "2026-09-10T18:59:59+00:00",
"timezone": "Asia/Almaty",
"total": 214,
"answered": 173,
"missed": 29,
"missed_not_called_back": 4,
"avg_talk_sec": 168,
"service_level": 82.5,
"service_level_threshold_sec": 20,
"ai_share": 21.4,
"directions": { "inbound": 141, "outbound": 73 },
"waiting_buckets": {
"median_sec": 12,
"buckets": [
{ "from_sec": 0, "to_sec": 10, "count": 48 },
{ "from_sec": 10, "to_sec": 20, "count": 41 },
{ "from_sec": 20, "to_sec": 40, "count": 19 },
{ "from_sec": 40, "to_sec": null, "count": 4 }
]
},
"by_day": [
{ "date": "2026-09-09", "answered": 27, "missed": 5 },
{ "date": "2026-09-10", "answered": 24, "missed": 3 }
],
"by_hour": [
{ "hour": 9, "total": 18, "answered": 11, "missed": 2 },
{ "hour": 10, "total": 23, "answered": 15, "missed": 3 }
],
"by_channel": [
{ "channel_id": 41, "channel_name": "SIP — основной номер", "provider": "sip", "total": 186 },
{ "channel_id": 7, "channel_name": "WhatsApp — Sales", "provider": "whatsapp", "total": 28 }
],
"by_assignee": [
{ "type": "operator", "id": 9, "name": "Данияр О.", "answered": 71, "missed": 12, "avg_talk_sec": 154 },
{ "type": "operator", "id": 14, "name": "Асем Б.", "answered": 65, "missed": 11, "avg_talk_sec": 181 },
{ "type": "ai_agent", "id": 3, "name": "Ассистент приёмной", "answered": 37, "missed": 0, "avg_talk_sec": 96 },
{ "type": null, "id": null, "name": null, "answered": 0, "missed": 6, "avg_talk_sec": null }
]
}
}Поиск слова в расшифровках звонков
Запрос
curl -G "https://api.aisar.app/v1/calls" \
--data-urlencode "q=возврат" \
--data-urlencode "q_in=all" \
--data-urlencode "date_from=2026-09-01" \
--data-urlencode "lazy_recordings=1" \
--data-urlencode "perPage=20" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Ответ
{
"data": {
"items": [
{
"id": 5121,
"direction": "inbound",
"status": "completed",
"provider": "sip",
"from_number": "7727000XXXX",
"to_number": "7008550XXXX",
"started_at": "2026-09-09T09:14:00+00:00",
"answered_at": "2026-09-09T09:14:08+00:00",
"ended_at": "2026-09-09T09:18:26+00:00",
"duration_sec": 258,
"contact": {
"id": 118,
"display_name": "Nurlan T.",
"avatar_url": null
},
"channel_id": 41,
"channel_name": "SIP — основной номер",
"channel_type": "sip_telephony",
"conversation_id": 921,
"timeline_message_id": 88250,
"recording_status": "stored",
"recording_url": null,
"has_recording": true,
"has_transcript": true,
"assignee": {
"id": 9,
"name": "Данияр О."
},
"transcript_matches": [
{
"at_sec": 34.5,
"role": "user",
"text": "…хочу оформить возврат по вчерашнему заказу…",
"highlights": [
{ "start": 15, "end": 22 }
]
}
]
}
],
"pagination": {
"page": 1,
"perPage": 20,
"total": 1,
"lastPage": 1
},
"meta": {
"search_truncated": false,
"search_cap": 500
}
}
}Поиск по расшифровкам без запроса (422)
Запрос
curl -G "https://api.aisar.app/v1/calls" \
--data-urlencode "q_in=transcript" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Ответ
{
"data": {
"message": "A search term is required when q_in is transcript or all.",
"errors": {
"q": [
"A search term is required when q_in is transcript or all."
]
}
}
}Список исходящих схем
Запрос
curl -G "https://api.aisar.app/v1/telephony/schemes" \
--data-urlencode "direction=outbound" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Ответ
{
"data": [
{
"id": "0199a1e2-7c3a-7f4e-9c2b-6a1d8e7b1c31",
"direction": "outbound",
"name": "Продажи",
"comment": null,
"status": "active",
"timezone": "Asia/Almaty",
"outbound_priority": 100,
"channels_count": 0,
"assigned_operators_count": 2,
"assigned_teams_count": 1,
"outbound_line": {
"channel_id": 41,
"name": "Городской 727",
"number": "+7727XXXXXXX"
},
"node_count": 3,
"updated_at": "2026-09-14T09:12:00+00:00"
}
]
}Превью линии дозвона
Запрос
curl -G "https://api.aisar.app/v1/telephony/outbound-line" \
--data-urlencode "to_number=+12125550181" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Ответ
{
"data": {
"scheme": {
"id": "0199a1e2-7c3a-7f4e-9c2b-6a1d8e7b1c31",
"name": "Продажи"
},
"blocked": false,
"block_reason": null,
"candidates": [
{
"channel_id": 41,
"name": "Городской 727",
"dial_mode": "dial_direct",
"is_default": true
},
{
"channel_id": 52,
"name": "Резервная линия",
"dial_mode": "dial_direct",
"is_default": false
}
],
"rejected": [
{ "channel_id": 9, "reason": "channel_not_connected" }
],
"allowed_channel_ids": [41, 52]
}
}Превью линии из embed-виджета (урезанный ответ)
Запрос
# Bearer token here carries an "embed:softphone:*" or "embed:read" ability
curl -G "https://api.aisar.app/v1/telephony/outbound-line" \
--data-urlencode "to_number=+12125550181" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Ответ
{
"data": {
"blocked": false,
"block_reason": null,
"default_channel_id": 41,
"dial_mode": "dial_direct",
"allowed_channel_ids": null
}
}Назначить операторов и команду на исходящую схему
Запрос
curl -X PUT "https://api.aisar.app/v1/telephony/schemes/0199a1e2-7c3a-7f4e-9c2b-6a1d8e7b1c31/assignments" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "operator_ids": [14, 27], "team_ids": [5] }'Ответ
{
"data": {
"assignments": {
"operators": [
{ "id": 14, "name": "Данияр О." },
{ "id": 27, "name": "Асем Б." }
],
"teams": [
{ "id": 5, "name": "Продажи" }
],
"conflicts": []
},
"moved": {
"operators": [],
"teams": []
}
}
}Политика исходящих компании: прочитать и изменить
Запрос
curl -X PATCH "https://api.aisar.app/v1/companies/current/telephony/outbound-settings" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "default_channel_id": 412, "manual_pick_enabled": false }'Ответ
{
"data": {
"default_channel_id": 412,
"default_channel": { "id": 412, "name": "Городской номер", "number": "+77010000000" },
"default_channel_available": true,
"manual_pick_enabled": false
}
}Звонок с номером вне основной схемы при выключенном выборе (422)
Запрос
curl -X POST "https://api.aisar.app/v1/calls/originate" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "contact_id": 9051, "channel_id": 413 }'Ответ
{
"data": {
"message": "Choosing the number before a call is turned off for this company.",
"error_code": "manual_pick_disabled"
}
}Попытка назначить операторов входящей схеме (422)
Запрос
curl -X PUT "https://api.aisar.app/v1/telephony/schemes/0199a1e2-99aa-7f4e-9c2b-6a1d8e7b1c00/assignments" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "operator_ids": [14], "team_ids": [] }'Ответ
{
"data": {
"message": "The given data was invalid.",
"errors": {
"scheme": [
"Only outbound schemes have operator assignments."
]
}
}
}Настройки записи разговоров компании
Запрос
curl "https://api.aisar.app/v1/companies/current/telephony/recording-settings" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Ответ
{
"data": {
"record_calls_enabled": true,
"retention_months": null,
"retention_options": [3, 6, 12],
"retention_changed_at": null,
"retention_effective_at": null,
"retention_paused_reason": null,
"last_changed_at": "2026-09-18T09:14:03.000000Z",
"last_changed_by": {
"user_id": 14,
"name": "Данияр О."
},
"archive": {
"bytes": 1157627904,
"calls_count": 776,
"measured_at": "2026-09-22T03:10:11.000000Z"
}
}
}Включить удаление записей старше 6 месяцев
Запрос
curl -X PATCH "https://api.aisar.app/v1/companies/current/telephony/recording-settings" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"retention_months": 6,
"confirm_retention_shortening": true
}'Ответ
{
"data": {
"record_calls_enabled": true,
"retention_months": 6,
"retention_options": [3, 6, 12],
"retention_changed_at": "2026-09-22T11:02:47.000000Z",
"retention_effective_at": "2026-09-30T02:40:00.000000Z",
"retention_paused_reason": "cooling_off",
"last_changed_at": "2026-09-22T11:02:47.000000Z",
"last_changed_by": {
"user_id": 14,
"name": "Данияр О."
},
"archive": {
"bytes": 1157627904,
"calls_count": 776,
"measured_at": "2026-09-22T03:10:11.000000Z"
}
}
}Тот же запрос без подтверждения (422, плоское тело)
Запрос
curl -X PATCH "https://api.aisar.app/v1/companies/current/telephony/recording-settings" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "retention_months": 6 }'Ответ
{
"message": "Shortening the retention term deletes recordings irreversibly and requires an explicit confirmation.",
"error_code": "retention_shortening_requires_confirmation"
}Что уничтожит срок в 6 месяцев (превью)
Запрос
curl -G "https://api.aisar.app/v1/companies/current/telephony/recording-settings/retention-preview" \
--data-urlencode "months=6" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Ответ
{
"data": {
"months": 6,
"affected_calls": 128,
"affected_bytes": 193273528,
"unmeasured_calls": 0,
"scrubbed_transcripts": 11,
"oldest_started_at": "2026-02-14T07:21:09.000000Z",
"first_deletion_at": "2026-09-30T02:40:00.000000Z"
}
}Создать входящую схему
Запрос
curl -X POST "https://api.aisar.app/v1/telephony/schemes" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "name": "Основная линия", "direction": "inbound", "timezone": "Asia/Almaty" }'Ответ
{
"data": {
"scheme": {
"id": "0199a1e2-7c3a-7f4e-9c2b-6a1d8e7b1c40",
"direction": "inbound",
"outbound_priority": 100,
"channels": [],
"name": "Основная линия",
"comment": null,
"timezone": "Asia/Almaty",
"status": "draft",
"moh_as_ringback": false,
"moh_prompt_id": null,
"nodes": [
{
"id": "0199a1e2-7c3b-7a10-8d11-2b3c4d5e6f70",
"type": "entry",
"position": { "x": 80, "y": 160 },
"data": []
}
],
"edges": []
}
}
}Загрузить аудио-приглашение
Запрос
curl -X POST "https://api.aisar.app/v1/telephony/audio-prompts" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-F "name=Приветствие" \
-F "file=@greeting.mp3"Ответ
{
"data": {
"item": {
"id": 12,
"name": "Приветствие",
"status": "ready",
"original_filename": "greeting.mp3",
"created_at": "2026-10-16T08:30:00+00:00"
}
}
}Граф входящей схемы: приветствие, расписание, IVR 1/2, две группы, голосовая почта
Запрос
curl -X PATCH "https://api.aisar.app/v1/telephony/schemes/0199a1e2-7c3a-7f4e-9c2b-6a1d8e7b1c40/builder" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"nodes": [
{ "id": "n_entry", "type": "entry", "position": { "x": 80, "y": 160 } },
{ "id": "n_hello", "type": "play", "position": { "x": 320, "y": 160 }, "data": { "prompt_id": 12 } },
{ "id": "n_hours", "type": "schedule", "position": { "x": 560, "y": 160 }, "data": { "intervals": [
{ "weekday": 1, "start": "09:00", "end": "18:00" },
{ "weekday": 2, "start": "09:00", "end": "18:00" },
{ "weekday": 3, "start": "09:00", "end": "18:00" },
{ "weekday": 4, "start": "09:00", "end": "18:00" },
{ "weekday": 5, "start": "09:00", "end": "18:00" }
] } },
{ "id": "n_menu", "type": "ivr_menu", "position": { "x": 800, "y": 120 }, "data": { "prompt_id": 13, "digits": ["1", "2"], "timeout_sec": 6, "repeats": 2 } },
{ "id": "n_sales", "type": "ring_group", "position": { "x": 1040, "y": 40 }, "data": { "members": { "operators": [], "teams": [3] }, "strategy": "all", "timeout_sec": 25 } },
{ "id": "n_support", "type": "ring_group", "position": { "x": 1040, "y": 200 }, "data": { "members": { "operators": [9, 14], "teams": [] }, "strategy": "sequential", "timeout_sec": 20 } },
{ "id": "n_vm", "type": "voicemail", "position": { "x": 1280, "y": 160 }, "data": { "greeting_id": 14, "max_sec": 120 } }
],
"edges": [
{ "source": "n_entry", "target": "n_hello", "sourceHandle": "out" },
{ "source": "n_hello", "target": "n_hours", "sourceHandle": "out" },
{ "source": "n_hours", "target": "n_menu", "sourceHandle": "work" },
{ "source": "n_hours", "target": "n_vm", "sourceHandle": "off" },
{ "source": "n_menu", "target": "n_sales", "sourceHandle": "digit_1" },
{ "source": "n_menu", "target": "n_support", "sourceHandle": "digit_2" },
{ "source": "n_menu", "target": "n_vm", "sourceHandle": "no_input" },
{ "source": "n_sales", "target": "n_vm", "sourceHandle": "no_answer" },
{ "source": "n_support", "target": "n_vm", "sourceHandle": "no_answer" }
]
}'Ответ
{
"data": {
"scheme": {
"id": "0199a1e2-7c3a-7f4e-9c2b-6a1d8e7b1c40",
"direction": "inbound",
"status": "draft",
"nodes": ["... 7 узлов с новыми uuid / 7 nodes with fresh uuids ..."],
"edges": ["... 9 рёбер / 9 edges ..."]
}
}
}Привязать канал и включить схему
Запрос
curl -X POST "https://api.aisar.app/v1/telephony/schemes/0199a1e2-7c3a-7f4e-9c2b-6a1d8e7b1c40/channels" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "channel_ids": [41] }'
curl -X PATCH "https://api.aisar.app/v1/telephony/schemes/0199a1e2-7c3a-7f4e-9c2b-6a1d8e7b1c40" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "status": "active" }'Ответ
{
"data": {
"message": "Scheme channels updated."
}
}