Giao thức WebSocket để AI voice bot tích hợp với Voice Gateway. Vendor cung cấp WebSocket server theo spec dưới đây; Gateway sẽ kết nối đến khi cuộc gọi bắt đầu và trao đổi audio/control event trong suốt cuộc gọi. Mô hình tham chiếu: Twilio Media Streams.
Version 1.0 — Audience: AI Voice Bot Vendor
Tích hợp gồm 2 phần: REST API để vendor yêu cầu Gateway quay số, và WebSocket để Gateway stream audio/control event với bot sau khi caller nhấc máy.
Half-duplex ở v1: Bot phát xong audio response thì Gateway mới forward audio của caller lên. Bot không cần xử lý barge-in trong phiên bản này.
VENDOR SIDE PLATFORM SIDE PSTN
┌──────────────────────────┐ ┌──────────────────────────────────┐ ┌────────┐
│ │ │ │ │ │
│ ┌────────────────────┐ │ 1. REST │ ┌────────────────────────────┐ │ SIP │ │
│ │ │──┼──────────────┼─►│ │─────────── ──► │
│ │ REST client │ │ POST │ │ Voice Gateway │ │ │ Caller │
│ │ (vendor app) │◄─┼──────────────┼──│ (REST server) │◄───────────── │
│ │ │ │ 200 OK │ │ │ │ │ │
│ └────────────────────┘ │ │ └──────────────┬─────────────┘ │ │ │
│ │ │ │ │ │ │
│ ┌────────────────────┐ │ │ │ 2. WS connect │ │ │
│ │ │◄─┼──────────────┼─────── WSS ─────┘ (sau khi caller│ │ │
│ │ AI Voice Bot │ │ │ nhấc máy) │ │ │
│ │ (WebSocket │──┼──────────────┼─────── WSS ─────────────────────►│ │ │
│ │ server) │ │ │ │ └────────┘
│ │ │ │ │ │
│ │ STT → LLM → TTS │ │ │ │
│ └────────────────────┘ │ │ │
│ │ │ │
└──────────────────────────┘ └──────────────────────────────────┘Thành phần | Vai trò |
|---|---|
Voice Gateway | WebSocket client (tới bot), đồng thời là REST server (nhận lệnh quay số từ vendor) |
AI Voice Bot | WebSocket server (nhận kết nối từ Gateway), đồng thời là REST client (gọi API quay số) |
Bước | Hành động | Mô tả |
|---|---|---|
1 | Vendor yêu cầu quay số qua REST API | Gọi |
2 | Gateway quay số qua SIP/PSTN | Gateway thực hiện outbound call tới caller. Nếu caller không nhấc máy → không có WebSocket nào được mở. |
3 | Gateway mở WebSocket tới bot | Sau khi caller nhấc máy, Gateway connect tới |
4 | Trao đổi audio/control | Bot gửi audio response ( |
5 | Kết thúc cuộc gọi | Caller cúp máy / bot gửi |
Ba scenario thường gặp: happy path có transfer, bot chủ động kết thúc, caller không nhấc máy.
AI Voice Bot Voice Gateway Caller
════════════ ═════════════ ══════
│ │ │
════ PHASE 0 · Vendor yêu cầu quay số (REST API) ═══════════════════════════════════════════════════════════
│ │ │
│ POST /v1/voice/callbot │ │
├───────────────────────────────────────────►│ │
│phone, campaignId, transactionId, socketUrl │ │
│ │ │
│ 200 OK {error_code: "success"} │ │
│◄───────────────────────────────────────────┤ │
│ │ │
│ │ Dial (SIP / PSTN) │
│ ├───────────────────────────────────────────►│
│ │ Nhấc máy │
│ │◄───────────────────────────────────────────┤
│ │ │
════ PHASE 1 · WebSocket handshake & start ═════════════════════════════════════════════════════════════════
│ │ │
│ WS connect │ │
│◄───────────────────────────────────────────┤ │
│ wss://.../ws/voice?api_key=... │ │
│ │ │
│ 101 Switching Protocols │ │
├───────────────────────────────────────────►│ │
│ │ │
│ connected │ │
│◄───────────────────────────────────────────┤ │
│ │ │
│ start │ │
│◄───────────────────────────────────────────┤ │
│ call_sid, metadata.custom │ │
│ │ │
════ PHASE 2 · Loop hội thoại (lặp cho mỗi lượt) ═══════════════════════════════════════════════════════════
│ │ │
╭── [ BOT NÓI ] ──────────────────────────────────────────────────────────────────────╮
│ │ │
│ media │ │
├───────────────────────────────────────────►│ │
│ audio chunks (PCM 8kHz) │ │
│ │ │
│ │ phát audio │
│ ├┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈►│
│ │ │
│ mark name: "turn_N_done" │ │
├───────────────────────────────────────────►│ │
│ [ chờ phát xong ] │
│ mark (echo) │ │
│◄───────────────────────────────────────────┤ │
╰─────────────────────────────────────────────────────────────────────────────────────╯
│ │ │
╭── [ CALLER NÓI ] ───────────────────────────────────────────────────────────────────╮
│ │ │
│ │ caller speak │
│ │◄┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┤
│ │ │
│ media track: "inbound" │ │
│◄───────────────────────────────────────────┤ │
╰─────────────────────────────────────────────────────────────────────────────────────╯
│ │ │
│ … bot xử lý và quay lại pha "Bot nói" … │
│ │ │
════ PHASE 3 · Chuyển cuộc gọi sang agent ══════════════════════════════════════════════════════════════════
│ │ │
│ transfer target: "agent_01" │ │
├───────────────────────────────────────────►│ │
│ │ │
│ │ SIP REFER / bridge → agent │
│ ├───────────────────────────────────────────►│
│ │ │
│ stop reason: "transferred" │ │
│◄───────────────────────────────────────────┤ │
│ │ │Về
mark: Bot gửimarksau mỗi đoạn audio, Gateway echo lạimarkkhi caller đã nghe xong. Bot dùng tín hiệu này để biết khi nào nên bật ASR xử lý phản hồi của caller.
AI Voice Bot Voice Gateway
════════════ ═════════════
│ │
════ Bot chủ động kết thúc cuộc gọi ════════════════════════════════════════════════════════════════════════
│ │
│ media (chunk cuối) │
├───────────────────────────────────────────►│
│ │
│ stop reason: "conversation_complete" │
├───────────────────────────────────────────►│
│ │
│ [ drain buffer ] │
│ phát nốt audio cho caller │
│ │
│ stop (ack) close code 1000 │
│◄───────────────────────────────────────────┤
│ │
AI Voice Bot Voice Gateway Caller
════════════ ═════════════ ══════
│ │ │
│ POST /v1/voice/callbot │ │
├───────────────────────────────────────────►│ │
│ │ │
│ 200 OK {error_code: "success"} │ │
│◄───────────────────────────────────────────┤ │
│ │ │
│ │ Dial (SIP / PSTN) │
│ ├───────────────────────────────────────────►│
│ │ │
╔════════════════════════════════════════════════════════════════════╗
║ ║
║ Không nhấc máy · máy bận · thuê bao tắt ║
║ ║
╚════════════════════════════════════════════════════════════════════╝
→ Không có WebSocket nào được mởLưu ý về phản hồi kết quả quay số: Ở v1, không có webhook trả kết quả dial cho vendor. Nếu caller không nhấc máy, vendor sẽ không nhận được thông tin — nên dùng
transactionIdđể đối soát, hoặc liên hệ team tích hợp để lấycall_sidkhi cần debug.
Vendor gọi API này khi muốn Gateway quay số ra cho caller (ví dụ: trong campaign outbound). API là async — trả về ngay khi nhận yêu cầu; quá trình quay số và kết nối WebSocket diễn ra sau.
{{baseUrl}}/v1/voice/callbotHeader X-Api-Key được cấp khi vendor đăng ký bot.
X-Api-Key: <api_key>
Content-Type: application/jsoncurl --location '{{baseUrl}}/v1/voice/callbot' \
--header 'X-Api-Key: {{api_key}}' \
--header 'Content-Type: application/json' \
--data-raw '{
"phone": "0123456789",
"campaignId": 1,
"transactionId": "TXN_004",
"socketUrl": "wss://bot.vendor.com/ws/voice?api_key=xxx",
"name": "John Doe",
"email": "[email protected]",
"address": "123 Main St, Anytown, USA",
"pField1": "Thông tin cá thể hoá 1",
"pField2": "Thông tin cá thể hoá 2",
"pField3": "Thông tin cá thể hoá 3",
"pField4": "Thông tin cá thể hoá 4",
"pField5": "Thông tin cá thể hoá 5",
"pField6": "Thông tin cá thể hoá 6"
}'const axios = require('axios')
const response = await axios.post(
'{{baseUrl}}/v1/voice/callbot',
{
phone: '0123456789',
campaignId: 1,
transactionId: 'TXN_004',
socketUrl: 'wss://bot.vendor.com/ws/voice?api_key=xxx',
name: 'John Doe',
email: '[email protected]',
address: '123 Main St, Anytown, USA',
pField1: 'Thông tin cá thể hoá 1',
pField2: 'Thông tin cá thể hoá 2',
pField3: 'Thông tin cá thể hoá 3',
pField4: 'Thông tin cá thể hoá 4',
pField5: 'Thông tin cá thể hoá 5',
pField6: 'Thông tin cá thể hoá 6'
},
{
headers: {
'X-Api-Key': '{{api_key}}',
'Content-Type': 'application/json'
}
}
)
console.log(response.data)import requests
response = requests.post(
'{{baseUrl}}/v1/voice/callbot',
json={
'phone': '0123456789',
'campaignId': 1,
'transactionId': 'TXN_004',
'socketUrl': 'wss://bot.vendor.com/ws/voice?api_key=xxx',
'name': 'John Doe',
'email': '[email protected]',
'address': '123 Main St, Anytown, USA',
'pField1': 'Thông tin cá thể hoá 1',
'pField2': 'Thông tin cá thể hoá 2',
'pField3': 'Thông tin cá thể hoá 3',
'pField4': 'Thông tin cá thể hoá 4',
'pField5': 'Thông tin cá thể hoá 5',
'pField6': 'Thông tin cá thể hoá 6'
},
headers={
'X-Api-Key': '{{api_key}}',
'Content-Type': 'application/json'
}
)
print(response.json())Field | Type | Bắt buộc | Mô tả |
|---|---|---|---|
| string | Có | Số điện thoại caller |
| int | Có | ID campaign đã cấu hình trên Gateway |
| string | Có | ID giao dịch phía vendor, dùng để đối soát |
| string | Có | URL WebSocket của bot — Gateway sẽ connect tới sau khi caller nhấc máy |
| string | Không | Tên khách hàng |
| string | Không | |
| string | Không | Địa chỉ |
| string | Không | Các trường cá thể hoá, sẽ được forward vào |
{
"error_code": "success",
"message": "OK"
}Response semantics: HTTP 200 +
error_code = "success"→ Gateway đã nhận yêu cầu và sẽ quay số. Các giá trịerror_codekhác đều là lỗi (saiapi_key, thiếu field, campaign không tồn tại, …).
Gateway chủ động mở WebSocket tới socketUrl mà vendor đã cung cấp ở bước khởi tạo cuộc gọi.
Gateway sẽ connect tới socketUrl mà vendor đã cung cấp trong API request. URL phải kèm api_key qua query param:
wss://bot.vendor.com/ws/voice?api_key=<KEY>Bot verify
api_keytrong handshake: Nếu sai → đóng WebSocket với close code1008(policy violation).
Yêu cầu | Giá trị |
|---|---|
Protocol | WebSocket (RFC 6455) |
Scheme |
|
Message format | JSON, text frames, UTF-8 |
Audio transport | Base64 trong field |
Timeout | Giá trị mặc định |
|---|---|
Connect timeout | 5 giây |
Idle timeout (không có media) | 30 giây |
Max session duration | 900 giây |
Kết thúc cuộc gọi: Khi cuộc gọi kết thúc, Gateway gửi event
stoprồi đóng WS với close code1000(normal). Vendor không cần reconnect.
Audio format cố định ở v1. Gateway và bot đều phải dùng đúng format này.
Thuộc tính | Giá trị |
|---|---|
Codec | PCM signed 16-bit little-endian ( |
Sample rate | 8000 Hz |
Channels | 1 (mono) |
Frame size | 20ms / chunk (160 samples = 320 bytes) |
Transport | Base64 string trong JSON |
Mọi message là JSON UTF-8 gửi qua WebSocket text frame.
connectedGATEWAY → BOT — Gửi ngay sau khi WebSocket handshake thành công.
{
"event": "connected",
"protocol": "voice_stream",
"version": "1.0"
}startGATEWAY → BOT — Gửi 1 lần duy nhất sau connected. Chứa metadata cuộc gọi. Vendor nên cache thông tin này trong suốt session.
{
"event": "start",
"sequence_number": 1,
"start": {
"stream_sid": "MZxxxxxxxxxxxxxxxxx",
"call_sid": "call-abc123",
"media_format": {
"encoding": "pcm_s16le",
"sample_rate": 8000,
"channels": 1
},
"metadata": {
"phone_number": "0900000000",
"direction": "outbound",
"custom": {
"key1": "value1",
"key2": "value2"
}
}
}
}Field | Type | Mô tả |
|---|---|---|
| string | ID duy nhất của WebSocket stream |
| string | ID cuộc gọi, dùng để log / trace |
| object | Luôn là PCM 8kHz mono s16le ở v1 |
| string | Số điện thoại của caller |
| string |
|
| object | Dynamic fields tuỳ cấu hình từng bot |
metadata.custom: Các trường động do vendor và Gateway thoả thuận trước khi tích hợp. Schema được định nghĩa khi đăng ký bot (ví dụ: mapping từpField1…pField6ở REST API).
mediaGATEWAY → BOT — Audio từ caller stream lên bot. Gửi liên tục ~20ms / chunk.
{
"event": "media",
"sequence_number": 42,
"media": {
"track": "inbound",
"chunk": 41,
"timestamp": 1776326027630,
"payload": "<base64_pcm_data>"
}
}Field | Type | Mô tả |
|---|---|---|
| string | Luôn |
| int | Số thứ tự chunk, bắt đầu từ 0 |
| int | Unix ms |
| string | Base64 của raw PCM bytes (320 bytes / chunk) |
markGATEWAY → BOT — Echo lại marker mà bot đã gửi. Gateway gửi mark lại cho bot khi audio tương ứng đã phát xong cho caller nghe.
{
"event": "mark",
"sequence_number": 80,
"mark": {
"name": "greeting_done"
}
}stopGATEWAY → BOT — Cuộc gọi kết thúc. Gateway gửi rồi đóng WS.
{
"event": "stop",
"sequence_number": 999,
"stop": {
"reason": "caller_hangup",
"call_sid": "call-abc123"
}
}
| Mô tả |
|---|---|
| Caller cúp máy |
| Bot yêu cầu dừng |
| Cuộc gọi đã transfer thành công |
| Idle timeout |
| Lỗi hệ thống |
Bot gửi audio response, marker, lệnh transfer hoặc stop về Gateway.
mediaBOT → GATEWAY — Audio response bot phát cho caller nghe.
{
"event": "media",
"media": {
"payload": "<base64_pcm_data>"
}
}Phải là PCM 8kHz mono s16le (đúng với start.media_format)
Chunk size nên 20–100ms để giảm latency
Không cần chunk / timestamp — Gateway tự sequence
markBOT → GATEWAY — Đặt checkpoint. Gateway sẽ echo lại khi audio trước nó đã phát xong cho caller.
{
"event": "mark",
"mark": {
"name": "question_1_done"
}
}Bot dùng để biết khi nào caller đã nghe xong audio, từ đó bật ASR lắng nghe phản hồi.
transferBOT → GATEWAY — Chuyển cuộc gọi sang agent hoặc queue.
{
"event": "transfer",
"transfer": {
"target": "agent_extension_or_queue",
"context": "default",
"on_complete": "hangup_bot"
}
}Field | Type | Mô tả |
|---|---|---|
| string | Extension / queue ID / phone number |
| string | Routing context, nhận từ Gateway khi đăng ký |
| string |
|
Sau khi nhận
transfer, Gateway sẽ:
Phát nốt audio trong buffer (drain)
Thực hiện transfer
Gửi
stopvới reasontransferredcho botĐóng WS
stopBOT → GATEWAY — Kết thúc cuộc gọi chủ động từ bot.
{
"event": "stop",
"stop": {
"reason": "conversation_complete"
}
}Gateway sẽ phát nốt audio trong buffer rồi kết thúc cuộc gọi.
Liên hệ team tích hợp Alohub khi cần hỗ trợ:
Review implementation: Sanity check spec và logic xử lý event trước khi lên production.
Cấu hình api_key & URL: Trên môi trường staging / production cho từng bot.
Debug cuộc gọi thật: Cần cung cấp call_sid hoặc transactionId để truy vết log.