WebSocket Streaming

Connect a custom AI voice agent to the Edesy Voice Platform over WebSocket — bidirectional L16 PCM audio, a JSON metadata frame, control commands, DTMF, and the outbound Calls API.

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