# 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.

Source: https://edesy.in/docs/sip-trunk-for-ai-voice-agents/connect/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](https://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:

```json
{
  "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:

```json
{"type": "killAudio"}
```

**End the call** — stop listening and disconnect:

```json
{"type": "disconnect"}
```

**Synchronization marker** — insert a named marker into the playback queue:

```json
{"type": "mark", "data": {"name": "utterance-1-end"}}
```

When the marker is reached during playout, the platform sends back:

```json
{"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:

```json
{"event": "dtmf", "dtmf": "2", "duration": "1600"}
```

## 2. Make an outbound call

```bash
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:

```json
{
  "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](https://edesy.in/docs/sip-trunk-for-ai-voice-agents/api-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.

```python
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](https://edesy.in/docs/sip-trunk-for-ai-voice-agents/api-reference/calls#status-webhooks).

## 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](https://edesy.in/docs/sip-trunk-for-ai-voice-agents/api-reference/calls) — outbound Calls API, status webhooks, hang up
- [SIP Termination Trunk](https://edesy.in/docs/sip-trunk-for-ai-voice-agents/connect/sip-termination-trunk) — connect a SIP-native platform instead
