Источник:
smarty-code/smarty-backend-stable/smarty-sip/docs/API.md
Дата актуальности: 2026-06-13
/{api}/ws/:_wsId/sipcall/...
Где {api} — config.rest.api (обычно /api), _wsId — ID рабочего пространства.
| Метод | Путь | Назначение | Вход | Выход | Ошибки |
|---|---|---|---|---|---|
GET |
/:_id |
Получить звонок | — | Объект SipCall (с EmployeeRelatedFields) |
403 (нет прав), 404 |
GET |
/ |
Список звонков | query: фильтры, пагинация | Массив звонков | 403 |
POST |
/dial/phone |
Звонок по номеру | body: { phone: string } |
204 | 403 (нет SIP-бинда), 402 (нет фичи) |
POST |
/dial/object |
Звонок по объекту CRM | body: { object: { _id, collectionName }, field: string } |
204 | 403, 404 (объект) |
POST |
/:_id/hide |
Скрыть окно вызова | — | 204 | 403 |
POST |
/:_id/objects |
Привязать объекты к звонку | body: { objects: [{_id, collectionName}] } |
204 | 403, 409 (уже есть привязки) |
GET |
/:_id/record/download |
Скачать запись | — | Redirect на файл | 404 (нет записи), 402 (квота) |
GET |
/:_id/record/view |
Ссылка на просмотр записи | — | URL (string) | 404, 402 |
GET |
/:_id/record/url |
Прямая ссылка на запись | — | URL (string) | 404, 402 |
Ограничения:
disabledMethods: ['create', 'delete'] — нельзя создать/удалить звонок через API.restrictProfiles('wsapi') — только авторизованные профили.ws.checkFeature('sipcall').employee.checkAccessRights(call, method).Валидация на границе:
dial/phone: phone — required, string.dial/object: field — required, string; object — валидация через ObjectInstanceValidator.objects: ObjectsListValidator проверяет, что объекты существуют и имеют телефон.Побочные эффекты:
dial/*: инициирует звонок через SIP-провайдера, создаёт SipCall (если провайдер вернул данные сразу).hide: добавляет сотрудника в hiddenForEmployees, отправляет DBCA-update.objects: привязывает объекты через addCallEvents, отправляет objectCases.add в organizer-events.| Метод | Путь | Назначение | Вход | Выход |
|---|---|---|---|---|
GET |
/sip-hook/:token |
Верификация Zadarma | query: zd_echo |
zd_echo (JSON) |
POST/GET/PUT/DELETE |
/sip-hook/:token |
Общее событие | body/query: данные провайдера | {} (JSON) |
POST/GET/PUT/DELETE |
/sip-hook/:token/:command |
Событие с командой | path: command |
{} |
POST/GET/PUT/DELETE |
/sip-hook/:token/:events/:command |
Событие с events/command | path: events, command |
{} |
POST |
/sip-hook/:token/:events/:command/:status |
Событие со статусом | path: status |
{} |
Аутентификация: NoAuthRequest + SipRequest — по hookToken в URL. Нет подписи, нет IP-whitelist.
Логика:
SipAccount по token.account.api.parseEvent(body, command, method) — парсинг события провайдером.result — сохранить через saveHookRequest('calls-sip', ...).ws.checkFeature('sipcall') !== false — отправить в очередь calls-sip.command === 'record' && isRecording → delayed event через 30с.Побочные эффекты:
hook_requests.calls-sip).Внимание:
meetings.jsне подключён вroutes/index.js(строка 7 закомментирована). Код существует, но не работает.MEETING_TYPESимпортируется изconstants.js, но не определён — latent bug.
| Метод | Путь | Назначение | Вход | Выход |
|---|---|---|---|---|
POST |
/meetings |
Создать встречу | body: { name, scheduledOn, endedOn, participants } |
201: объект встречи |
GET |
/meetings |
Список встреч | query: _wsId, status, limit, offset |
{ data, total, limit, offset } |
GET |
/meetings/:_id |
Получить встречу | — | Объект встречи |
POST |
/meetings/:_id/join |
Присоединиться | body: { employeeId? } |
{ success, sipUri, joinUrl } |
POST |
/meetings/:_id/end |
Завершить встречу | — | { success, endedOn } |
DELETE |
/meetings/:_id |
Удалить встречу | — | { success } |
Права: checkAdminRights('manage_calls'), checkAllConfirm().
| Очередь | Сервис | Назначение |
|---|---|---|
sipcalls |
worker.js |
Online/offline профилей → обновление списков звонков |
calls-sip |
sip-service.js |
События от SIP-провайдеров (webhooks) |
calls-webrtc |
webrtc-service.js |
Клиентские WebRTC-действия + системные таймеры |
| Событие | Источник | Payload | Эффект |
|---|---|---|---|
incomingCall |
hooks.js |
{ _sipAccount, phone, rawExtId, rawExtContent, targetExtId } |
SipCall.callUpsert('incoming', ...): создание/обновление звонка, привязка объектов по телефону |
outgoingCall |
hooks.js |
同上 | SipCall.callUpsert('outgoing', ...) |
answerCall |
hooks.js |
{ _sipAccount, rawExtId, targetExtId } |
call.callAnswer(bind): установка _employeeAnswered, callStatus='in_process', callResult='answered' |
endCall |
hooks.js |
{ _sipAccount, rawExtId, targetExtId, duration, status, link?, isRecording } |
call.callEnd(duration, status): завершение звонка, скачивание записи (если link) |
callRecordFileGet |
hooks.js |
{ _sipAccount, rawExtId, link } |
callRecordDownload: delayed event в image_processing через 60с |
callRecordFileBind |
image_processing |
{ _callId, _fileId } |
Привязка файла к звонку, DBCA-update record |
callRecordFileClear |
— | { ids: [_fileId] } |
Очистка _fileId у звонков |
clearEmployee |
— | { ids: [_employeeId], _wsId } |
Очистка _employeeId у SipEmployee, скрытие звонков |
clearBinding |
SipEmployee.actualizeNumbers |
{ _bindId: { _employeeId: _bindId } } |
Скрытие звонков для сотрудников |
phonesActualize |
— | { _id, modelName, source } |
Обновление привязок объектов к звонкам при изменении телефонов |
hook-history |
— | — | No-op (заглушка) |
| Событие | Источник | Payload | Эффект |
|---|---|---|---|
create |
Клиент (socket) | { _userId, _employeeId, employeeIds, ... } |
WebRtcCall.createObject(): создание конференции, instance-pre-validate (callLimit, участники), instance-post-create (addParticipants) |
join |
Клиент | { _callId, socketSessionId, callToken, audio, video } |
joinConference: participantCameOnline, setStatus('dialing'/'in_process'), таймер callLimit |
leave |
Клиент | { _callId } |
leaveConference('left'): participantGoneOffline, завершение если последний |
reject |
Клиент | { _callId } |
leaveConference('reject'): отклонение вызова |
offline |
Система (socket disconnect) | { _userId, _employeeId } |
participantOfflineHandle: leaveConference('offline'), таймер webrtc_offline_timeout |
invite |
Клиент | { _callId, employeeIds } |
inviteParticipants: добавление участников (только инициатор), buildInviteableEmployeesList |
ice |
Клиент | { _callId, target, candidate } |
Пересылка ICE-кандидата целевому участнику через socket.io |
meta |
Клиент | { _callId, socketSessionId, callToken, audio, video } |
updateParticipantMeta: обновление метаданных участника |
webrtc_dial_hangup |
Система (delayed) | { _callId, _employeeId, _userId } |
Автоотбой при дозвоне (60с для online, 1с для offline) |
webrtc_offline_timeout |
Система (delayed) | { _callId, _employeeId } |
Автозавершение при offline (30с) |
webrtc_auto_hangup |
Система (delayed) | { _callId } |
Автозавершение по callLimit |
callMissed |
Система | { _callId } |
Отправка уведомлений CallMissed участникам |
callFinished |
Система | { _callId } |
Уведомление sipcallFinishedMessage в dialog-events |
| Событие | Источник | Payload | Эффект |
|---|---|---|---|
online |
Auth-сервис | { socketSessionId, _userId } |
Отправка currentCallsList (WebRTC + SIP) через socket.io и updateStat |
offline |
Auth-сервис | { _userId } |
Скрытие завершённых звонков для сотрудников пользователя |
Клиент → socket.emit('sipAction', { action: 'create', data: { employeeIds: [...] } })
→ webrtc-service (calls-webrtc)
→ callEventWrapper → clientEvents.webrtcEvents → sipAction('create')
→ WebRtcCall.createObject(employee, data)
→ instance-pre-validate:
- buildInviteableEmployeesList: проверка лимитов, прав (allow_incoming_calls)
- callLimit из тарифа workspace
→ instance-post-create:
- addParticipants: создание WebRtcParticipant, delayed webrtc_dial_hangup
- checkAvailableParticipants: если все offline → setStatus('finished'), callMissed
→ socket.io → участникам
Клиент → socket.emit('sipAction', { action: 'join', data: { _callId, callToken, audio, video } })
→ joinConference(employee, socketSessionId, meta)
→ participantCameOnline: status='in_call', joinTime
→ Если инициатор && status='pending': setStatus('dialing')
→ Иначе: clearDelayedEvent(webrtc_dial_hangup), setStatus('in_process')
→ Если callLimit: delayed webrtc_auto_hangup
→ dbcaUpdate: participants, call_credentials
Клиент A → ice({ _callId, target: employeeB, candidate })
→ call.getParticipant(target)
→ socket.io.to(participant.socketSessionId).emit('ice', { _callId, from, candidate })
| Код | Класс | Условие |
|---|---|---|
| 400 | ValidationError |
Невалидные входные данные, неизвестный статус |
| 402 | QuotasError |
Квота на запись превышена (file.overRate) |
| 403 | ForbiddenError / AccessDenied |
Нет прав, нет SIP-бинда, лимит участников, нет фичи |
| 404 | NotFoundError |
Звонок/запись/аккаунт не найден |
| 409 | ForbiddenError |
Звонок уже имеет привязанные объекты |
| Параметр | Значение | Описание |
|---|---|---|
PARTICIPANTS_LIMIT |
50 | Максимум участников в WebRTC-конференции |
DIAL_DELAY |
60 000 мс | Таймаут дозвона для online-пользователей |
HANGUP_DELAY |
30 000 мс | Таймаут переподключения при offline |
CREDENTIALS_TIMEOUT |
28 800 с (8 ч) | TTL TURN-credentials |
callLimit |
из тарифа | Ограничение длительности звонка (null = безлимита) |
| Запись звонка | 30 с задержка | Delayed event для record (ожидание завершения загрузки) |
callStatusList)| Статус | Описание | Допустимые переходы |
|---|---|---|
pending |
Ожидание ответа | → dialing, finished |
dialing |
Дозвон | → in_process, finished |
in_process |
Активный разговор | → finished |
finished |
Завершён | — (терминальный) |
callResultList)| Результат | Описание |
|---|---|
none |
Не определён |
answered |
Отвечен |
busy |
Занято |
cancel |
Отменён |
missed |
Пропущен |
failed |
Не удался |
not-allowed |
Запрещено |
unallocated |
Номер не существует |
unavailable |
Абонент недоступен |
error |
Ошибка |
WebRtcParticipant.status)| Статус | Описание |
|---|---|
pending |
Ожидание ответа |
in_call |
В разговоре |
left |
Вышел |
offline |
Потеряна связь |
reject |
Отклонил / таймаут |
| Событие | Payload | Описание |
|---|---|---|
sipAction |
{ action: string, data: object } |
Диспетчеризация через webrtcEvents |
Действия: create, join, leave, reject, invite, ice, meta.
| Событие | Payload | Описание |
|---|---|---|
currentCallsList |
[{ ...callInfo }] |
Список активных звонков при online |
ice |
{ _callId, from, candidate } |
ICE-кандидат от другого участника |
sip_error |
{ action, error } |
Ошибка при обработке действия |
meetings.js мёртв. Код существует, но не подключён. MEETING_TYPES не определён в constants.js.
removeDeadCalls в accessChecker. При create звонка проверяются активные звонки пользователя, и «мёртвые» (без socketSessionId + joinTime, старше 60с/30мин) принудительно завершаются. Побочный эффект в read-операции.
participantsCache (WeakMap). Передаёт данные между instance-pre-validate и instance-post-create. Если create выбросит ошибку между хуками, кэш может содержать stale-данные до GC.
callUpsert идемпотентен по rawExtId. Повторное событие с тем же rawExtId обновит существующий звонок, но fixedEntries защищает от перезаписи привязок.
answerCall без звонка. Если SipCall.findOne({ rawExtId }) вернёт null — событие молча игнорируется. Нет retry, нет логирования ошибки.
endCall без answerCall. Если провайдер не присылает answerCall, _employeeAnswered определяется постфактум в endCall (по targetExtId или из employeeHistory).
hook-history — no-op обработчик. Предназначен для чего-то, но не реализован.
clearEmployee vs clearBinding. clearEmployee очищает _employeeId у SipEmployee, clearBinding — скрывает звонки. Вызываются вместе, но могут быть рассинхронизированы.
phoneToSearch — последние 10 цифр. Для международных номеров возможна коллизия. Нет валидации формата.
isRecording в endCall. Если true — callEnd не вызывается. Запись обрабатывается отдельно через callRecordFileGet + delayed event.