WebSocket Streaming
For a custom agent, the Edesy Voice Platform streams real-time, bidirectional audio to your agent over a WebSocket. Edesy handles all SIP signalling, call routing, and media processing — your agent just accepts WebSocket connections and processes audio.
Your AI agent (WebSocket server)
▲
│ WebSocket (wss://)
│ - JSON metadata frame (call info)
│ - Binary L16 PCM audio (bidirectional)
│
Edesy Voice Platform
▲
│
Phone Network (PSTN)
Prerequisites
| Item | Value |
|---|---|
| API base URL | https://voice-api.edesy.in |
| Account SID | Provided with your account |
| API key (Bearer token) | Provided with your account (vp_…) |
| Application SID | Provided with your account |
| From number | Your provisioned number (e.g. 917969002802) |
Find your Account SID, API key, and Application SID in the portal at voice-app.edesy.in.
1. Build your WebSocket voice agent
Your agent is a WebSocket server that accepts connections from the Edesy Voice Platform at a fixed URL (e.g. wss://your-agent.example.com/ws). All calls connect to the same URL — identify each call from the metadata sent in the first message.
Connection lifecycle
Edesy Voice Platform Your AI agent
│ │
│──── WebSocket upgrade GET ────────────>│
│<─── 101 Switching Protocols ───────────│
│ │
│──── TEXT: initial metadata (JSON) ────>│ (1) call info
│──── BINARY: L16 PCM audio ───────────>│ (2) caller audio (continuous)
│<─── BINARY: L16 PCM audio ─────────────│ (3) agent audio (bidirectional)
│<─── TEXT: {"type":"killAudio"} ────────│ (4) barge-in
│<─── TEXT: {"type":"disconnect"} ───────│ (5) end call
│──── WebSocket close ──────────────────>│ (6) call ended
Initial metadata frame
The first message after connection is a JSON text frame with call information:
{
"call_sid": "f4590c42-1a14-4a5f-b526-32f2a4c09018",
"account_sid": "d7e8f9a0-b1c2-3d4e-5f60-a1b2c3d4e5f6",
"application_sid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"direction": "inbound",
"from": "919876543210",
"to": "917969002802",
"caller_name": "VOICELINK",
"call_id": "88d12f9a-2e4b-4c5f-9831-a3b2c1d4e5f6",
"sip_status": 200,
"call_status": "in-progress",
"sampleRate": 16000,
"mixType": "mono",
"callSid": "f4590c42-1a14-4a5f-b526-32f2a4c09018",
"callerName": "VOICELINK"
}
Key fields:
| Field | Description |
|---|---|
call_sid |
Unique identifier for this call leg |
from |
Caller's phone number |
to |
Called phone number |
direction |
"inbound" or "outbound" |
callSid |
Same as call_sid |
callerName |
SIP caller name |
sampleRate |
Audio sample rate for this connection |
Audio format
| Direction | Format | Encoding | Sample rate | Frame size |
|---|---|---|---|---|
| Platform → agent | Binary WebSocket | L16 PCM (signed 16-bit LE) | 16000 Hz | ~640 bytes/frame (~20 ms) |
| Agent → platform | Binary WebSocket | L16 PCM (signed 16-bit LE) | 16000 Hz | ~640 bytes/frame (~20 ms) |
Control commands (agent → platform)
Send JSON text frames to control the call.
Interrupt / barge-in — flush all queued playback audio:
{"type": "killAudio"}
End the call — stop listening and disconnect:
{"type": "disconnect"}
Synchronization marker — insert a named marker into the playback queue:
{"type": "mark", "data": {"name": "utterance-1-end"}}
When the marker is reached during playout, the platform sends back:
{"type": "mark", "data": {"name": "utterance-1-end", "event": "playout"}}
DTMF events (platform → agent)
If DTMF passthrough is enabled, digit presses arrive as JSON text frames:
{"event": "dtmf", "dtmf": "2", "duration": "1600"}
2. Make an outbound call
curl -X POST https://voice-api.edesy.in/v1/Accounts/{ACCOUNT_SID}/Calls \
-H "Authorization: Bearer {API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"from": "917969002802",
"to": {
"type": "phone",
"number": "919876543210"
},
"application_sid": "{APPLICATION_SID}",
"timeout": 60
}'
Response:
{
"sid": "f4590c42-1a14-4a5f-b526-32f2a4c09018"
}
The sid is the outbound call_sid. Store it — see the mapping note below. Full field and response reference: Calls.
3. Map outbound calls to WebSocket sessions
When the callee answers, a WebSocket connection arrives at your agent with a different call_sid than the sid returned by POST /Calls — they are different call legs. Match them by the platform from-number using FIFO ordering (oldest pending session first), not by direct call_sid lookup.
from collections import defaultdict
pending_by_number = defaultdict(list) # from_number -> [session_data, ...]
# After POST /Calls returns, store session keyed by the platform from-number:
pending_by_number[FROM_NUMBER].append({
"outbound_call_sid": outbound_call_sid,
"session_id": session_id,
})
# In the WebSocket handler, match the newest connection to the oldest pending session:
metadata = await ws.receive_json()
to_num = (metadata.get("to") or "").replace("+", "").lstrip("0")
queue = pending_by_number.get(to_num, [])
session = queue.pop(0) if queue else None
Alternatively, use the trying status webhook (which fires before the WebSocket connects and carries your tag as customerData) to pre-register the session — see Calls.
Local development
If your agent runs locally, expose it with a tunnel so the platform can reach it, and provide the public WebSocket URL (e.g. wss://your-subdomain.example.dev/ws). The URL must be publicly reachable.
Next
- Calls — outbound Calls API, status webhooks, hang up
- SIP Termination Trunk — connect a SIP-native platform instead