Звонок ведёт сервер Gratio: приложение ответило на входящий через answerCall или позвонило через makeCall. Каждое изменение состояния такого звонка приходит уведомлением callStateChanged, один раз.
typeWebhook |
callStateChanged |
typeMessage |
— |
| Флаг настроек | incomingCallWebhook |
| Каналы | MAX, WhatsApp — где getCapabilities отвечает call_audio: true |
Пример
Разговор с Марией закончился: она положила трубку через 21,7 секунды, разговор записан:
{
"typeWebhook": "callStateChanged",
"instanceData": {"idInstance": 7100000001, "wid": "79990000001@c.us", "typeInstance": "v3", "channel": "max"},
"timestamp": 1791988832,
"idMessage": "31A9ADA2-3889-4B2B-9D88-6A7142875D7F",
"callId": "31A9ADA2-3889-4B2B-9D88-6A7142875D7F",
"chatId": "8695836",
"direction": "in",
"state": "ended",
"answered": true,
"endReason": "hangup",
"endedBy": "remote",
"talkSeconds": 21.7,
"recording": {
"downloadUrl": "https://api.gratio.net/calls/7100000001/31A9ADA2-3889-4B2B-9D88-6A7142875D7F/5b1e….wav",
"mimeType": "audio/wav",
"durationSeconds": 21.7
},
"eventId": "callstate:31A9ADA2-3889-4B2B-9D88-6A7142875D7F:ended"
}
Поля
Поля на верхнем уровне — объект звонка, тот же, что отвечают getCall и getActiveCalls:
| Поле | Описание |
|---|---|
callId |
id звонка; idMessage — то же значение. У входящего — idMessage из incomingCall (см. incomingCall и outgoingCall) со status: "offer" |
chatId |
собеседник |
direction |
in — ответили на входящий, out — позвонили через makeCall |
state |
ringing, connecting, active или ended — таблица ниже |
answered |
true — звук шёл, разговор состоялся |
endReason |
почему звонок закончился; null, пока он идёт |
endedBy |
кто закончил: remote — собеседник, api — приложение, system — сервер; null, пока звонок идёт |
talkSeconds |
длительность разговора в секундах, с дробной частью |
recording |
запись разговора — только в ended, если писали |
eventId |
callstate:{callId}:{state} — каждое состояние приходит один раз |
recording.downloadUrl — подписанная ссылка на WAV (голос собеседника, моно, 16 кГц), скачивается без токена.
Состояния
Состояния идут по порядку, каждое — одно уведомление:
входящий: answerCall ─► connecting ─► active ─► ended
исходящий: makeCall ───► ringing ────► connecting ─► active ─► ended
state |
Что значит |
|---|---|
ringing |
исходящий звонок звонит у собеседника |
connecting |
собеседник ответил или вы ответили, звук ещё не пошёл |
active |
разговор идёт, звук в обе стороны |
ended |
звонок закончился — причина в endReason |
Причины конца
endReason |
Что случилось |
|---|---|
hangup |
положили трубку |
rejected |
звонок отклонили |
canceled |
звонок отменили до ответа |
busy |
собеседник занят |
no_answer |
собеседник не ответил за 45 секунд |
max_duration |
разговор дошёл до maxDurationSeconds, сервер положил трубку |
failed |
не удалось соединиться или передать звук |
connection_lost, disconnected |
связь оборвалась |
restart |
Gratio обновлялся, а разговор не закончился за 2 минуты |
Как обработать
Сохранить запись разговора, когда звонок закончился:
import requests
def on_call_state(body: dict) -> None:
if body["typeWebhook"] != "callStateChanged":
return
if body["state"] != "ended":
update_call(body["callId"], state=body["state"])
return
update_call(body["callId"], state="ended", reason=body["endReason"], seconds=body["talkSeconds"])
recording = body.get("recording")
if recording:
r = requests.get(recording["downloadUrl"], timeout=120) # без токена: ссылка подписанная
r.raise_for_status()
with open(f"call-{body['callId']}.wav", "wb") as out:
out.write(r.content)
Что дальше
- Звонки: как это работает — ответ, исходящий звонок, звук.
- callAudio — живой звук звонка через WebSocket.
- incomingCall и outgoingCall — события звонков без участия сервера.
