Во время разговора, который ведёт сервер Gratio, приложение слушает собеседника и говорит ему в реальном времени: для голосового бота, распознавания речи или своей записи разговора.
| Запрос | wss://api.gratio.net/waInstance{idInstance}/callAudio/{apiTokenInstance}?callId={callId} |
| Протокол | WebSocket |
| Каналы | MAX, WhatsApp — если в getCapabilities call_audio: true |
Параметры
| Параметр | Где | Обязательный | Описание |
|---|---|---|---|
callId |
строка запроса | да | id звонка из answerCall, makeCall или incomingCall |
token |
строка запроса | нет | ключ для адреса /v1/…, если не передаёте заголовок Authorization |
Подключение
Подключайтесь после answerCall или makeCall, когда звонок в connecting или active. Адрес в стиле Green API:
wss://api.gratio.net/waInstance{idInstance}/callAudio/{apiTokenInstance}?callId={callId}
Или ключ — заголовком:
wss://api.gratio.net/v1/instances/{idInstance}/callAudio?callId={callId}
Authorization: Bearer {apiTokenInstance}
Вместо заголовка можно добавить к адресу &token={apiTokenInstance}.
Кадры от сервера
| Кадр | Что в нём |
|---|---|
текст start |
первым кадром: формат звука |
| двоичный | голос собеседника: PCM s16le, моно, 16 кГц, примерно по 20 мс — около 640 байт |
текст error |
звук приложения не принят: audio_queue_full — в очереди уже 5 минут |
текст end |
звонок закончился; затем сервер закрывает сокет с кодом 1000 |
Первый кадр:
{"event": "start", "callId": "31A9ADA2-3889-4B2B-9D88-6A7142875D7F", "sampleRate": 16000, "channels": 1, "format": "s16le"}
Последний кадр — endReason как в объекте звонка, null, если причина неизвестна:
{"event": "end", "callId": "31A9ADA2-3889-4B2B-9D88-6A7142875D7F", "endReason": "hangup"}
Очередь звука переполнена:
{"event": "error", "code": "audio_queue_full", "message": "в очереди уже 300 с звука"}
Медленный слушатель теряет старые кадры: сервер держит для него около 5 секунд звука. Разговор от этого не страдает.
Кадры от приложения
| Кадр | Что делает |
|---|---|
| двоичный | что сказать собеседнику: тот же формат — PCM s16le, моно, 16 кГц. Встаёт в очередь, как playAudio. Удобно слать по 20 мс — 640 байт |
{"command": "clear"} |
сбросить очередь — собеседник перебил |
{"command": "hangup"} |
положить трубку |
Закрыли сокет — разговор продолжается: подключитесь снова или положите трубку через hangUpCall.
Пример: ответить, записать и положить трубку
Скрипт отвечает на входящий звонок, говорит приветствие, пишет голос собеседника в WAV и через 20 секунд кладёт трубку. Нужны Python 3.10+ и pip install requests websockets. Кроме GRATIO_URL, GRATIO_ID и GRATIO_TOKEN задайте GREETING_URL — ссылку на файл приветствия (mp3, wav или ogg), например из uploadFile.
Запуск — с callId из уведомления incomingCall со status: "offer", пока звонок звонит:
python3 answer_call.py 31A9ADA2-3889-4B2B-9D88-6A7142875D7F
Файл answer_call.py:
"""Ответить на звонок, сказать приветствие, записать собеседника в WAV и положить трубку."""
import asyncio
import contextlib
import json
import os
import sys
import wave
import requests
import websockets
from websockets.exceptions import ConnectionClosed, InvalidHandshake
API = os.environ["GRATIO_URL"] # https://api.gratio.net
ID = os.environ["GRATIO_ID"]
TOKEN = os.environ["GRATIO_TOKEN"]
GREETING_URL = os.environ["GREETING_URL"] # приветствие: mp3, wav или ogg по ссылке
TALK_SECONDS = 20 # столько слушаем после подключения, потом кладём трубку
def call_method(name: str, payload: dict) -> dict:
url = f"{API}/waInstance{ID}/{name}/{TOKEN}"
response = requests.post(url, json=payload, timeout=60)
data = response.json()
if response.status_code != 200:
sys.exit(f"{name}: {response.status_code} {data.get('code')} {data.get('message')}")
return data
async def listen(call_id: str, path: str) -> None:
# https://… → wss://…, http://… → ws://…
url = "ws" + API.removeprefix("http") + f"/waInstance{ID}/callAudio/{TOKEN}?callId={call_id}"
async with websockets.connect(url) as ws:
start = json.loads(await ws.recv()) # первый кадр — формат звука
print("Звук пошёл:", start["format"], start["sampleRate"], "Гц")
async def hang_up_later() -> None:
await asyncio.sleep(TALK_SECONDS)
with contextlib.suppress(ConnectionClosed):
await ws.send(json.dumps({"command": "hangup"}))
timer = asyncio.create_task(hang_up_later())
with wave.open(path, "wb") as wav:
wav.setnchannels(start["channels"])
wav.setsampwidth(2) # s16le — 2 байта на отсчёт
wav.setframerate(start["sampleRate"])
try:
async for frame in ws:
if isinstance(frame, bytes): # голос собеседника, около 20 мс
wav.writeframes(frame)
continue
event = json.loads(frame)
if event["event"] == "end":
print("Звонок закончился:", event["endReason"])
break
print("Событие:", event)
finally:
timer.cancel()
def main() -> None:
call_id = sys.argv[1]
call = call_method("answerCall", {
"callId": call_id,
"playUrl": GREETING_URL,
"record": False, # пишем сами, запись на сервере не нужна
"maxDurationSeconds": TALK_SECONDS + 60,
})
print("Ответили, состояние:", call["state"])
path = f"call-{call_id}.wav"
try:
asyncio.run(listen(call_id, path))
except InvalidHandshake as e:
sys.exit(f"Сокет не открылся: {e}")
print("Голос собеседника —", path)
if __name__ == "__main__":
main()
Вы увидите:
Ответили, состояние: connecting
Звук пошёл: s16le 16000 Гц
Звонок закончился: hangup
Голос собеседника — call-31A9ADA2-3889-4B2B-9D88-6A7142875D7F.wav
Голосовой бот устроен так же: кадры собеседника — в распознавание речи, ответ синтезом — двоичными кадрами в тот же сокет, clear — когда собеседник перебивает. Пошагово — в руководстве Звонки: автоответчик и голосовой бот.
Отказ в подключении
Сервер проверяет ключ и звонок до установки соединения. При отказе рукопожатие WebSocket не проходит: сервер отвечает HTTP 403, кода закрытия у такого отказа нет. Браузер покажет закрытие с кодом 1006.
| Причина | Что делать |
|---|---|
неверный idInstance или токен |
проверить ключ канала |
разговора с таким callId сейчас нет: закончился или ещё не принят |
проверить getCall и getActiveCalls |
| канал не на связи | проверить getStateInstance |
| у канала нет звука звонков | проверить call_audio в getCapabilities |
| канал временно недоступен, например при обновлении сервиса | повторить через несколько секунд |
Если звонок закончился в момент подключения, сервер может открыть сокет и сразу прислать end.
Особенности каналов
- MAX и WhatsApp — один формат звука: PCM s16le, моно, 16 кГц.
- Telegram. Звука звонков нет — подключение отклоняется.
- Звонок через API — только голосовой: видео не передаётся.
Что дальше
- answerCall — ответить, чтобы подключиться к звуку.
- playAudio — сказать файлом вместо потока.
- Звонки: автоответчик и голосовой бот — руководство с распознаванием и синтезом.
