AgentStream
Stream real-time audio between live phone calls and your bot server over WebSocket. Three ways to connect โ pick the one that fits your setup.
How it worksโ
Caller โ Exophone โ Exotel โ wss://your-bot-server
When a call is answered, Exotel opens a WebSocket to your endpoint and streams raw PCM audio every ~100 ms. Send audio back on the same socket to speak to the caller.
Connect Voice AIโ
Dial a number and connect the answered call directly to your bot. No flow setup needed.
POST /v1/accounts/{account_sid}/calls/connect
| Region | Base URL |
|---|---|
| Mumbai | https://api.in.exotel.com |
| Singapore | https://api.exotel.com |
Auth: HTTP Basic โ API key as username, API token as password.
Parametersโ
| Parameter | Required | Description |
|---|---|---|
from | Yes | Number to dial โ E.164 format (+919876543210) |
callerid | Yes | Your Exophone (shown as caller ID) |
streamurl | Yes | Bot WebSocket URL โ wss:// or ws://, max 600 chars |
streamtype | Yes | bidirectional |
record | No | true to record the call |
recordingchannels | No | single (merged) or dual (separate tracks) |
timelimit | No | Max duration in seconds (max 14400) |
customfield | No | Metadata string, max 128 chars |
statuscallback | No | Webhook URL for call status events |
statuscallbackevents[] | No | answered ยท terminal ยท ringing |
streamname | No | Label for the stream, max 32 chars |
Requestโ
curl -X POST \
'https://<api_key>:<api_token>@api.in.exotel.com/v1/accounts/<account_sid>/calls/connect' \
-F 'from=+919876543210' \
-F 'callerid=08047491899' \
-F 'streamurl=wss://bot.example.com/media' \
-F 'streamtype=bidirectional' \
-F 'statuscallback=https://your-server.com/callback' \
-F 'statuscallbackevents[]=terminal'
Responseโ
{
"call": {
"sid": "a1b2c3d4e5f6",
"status": "in-progress",
"from": "+919876543210",
"phonenumbersid": "08047491899",
"direction": "outbound-api",
"datecreated": "2026-05-14 10:00:00",
"recordingurl": null
}
}
Status values: queued ยท in-progress ยท completed ยท failed ยท busy ยท no-answer
Connect Voice API with Flowโ
Dial a number and run the call through an Exotel flow โ IVR menus, greetings, DTMF collection โ before reaching your bot.
Same endpoint:
POST /v1/accounts/{account_sid}/calls/connect
Parametersโ
| Parameter | Required | Description |
|---|---|---|
from | Yes | Number to dial โ E.164 format |
callerid | Yes | Your Exophone |
url | Yes | Flow URL: https://my.exotel.com/{account_sid}/exoml/start_voice/{flow_id} |
calltype | No | trans for transactional calls |
timelimit | No | Max duration in seconds (max 14400) |
timeout | No | Ring timeout in seconds |
statuscallback | No | Webhook URL for status events |
statuscallbackevents | No | terminal ยท answered ยท both |
customfield | No | Passed into the flow via the Passthru applet |
Don't pass streamurl or streamtype here โ the WebSocket URL is configured inside the flow's Voicebot/Stream applet.
Requestโ
curl -X POST \
'https://<api_key>:<api_token>@api.in.exotel.com/v1/accounts/<account_sid>/calls/connect' \
-d 'from=+919876543210' \
-d 'callerid=08047491899' \
-d 'url=https://my.exotel.com/<account_sid>/exoml/start_voice/<flow_id>' \
-d 'statuscallback=https://your-server.com/callback' \
-d 'statuscallbackevents=terminal'
Responseโ
Same shape as Connect Voice AI response.
Common flow patternsโ
| Pattern | Applets in flow |
|---|---|
| Greeting โ bot | Play โ Voicebot |
| IVR menu โ bot | Gather (DTMF) โ Voicebot |
| Bot โ human agent | Voicebot โ Transfer |
| Listen-only (transcription) | Stream โ Passthru |
Programmable Voice APIs (ExoML)โ
Full runtime control via Legs API โ per-call stream URLs, dynamic routing, greeting + stream in parallel.
Step 1 โ Dial the customerโ
curl -X POST \
'https://<api_key>:<api_token>@api.in.exotel.com/v2/accounts/<account_sid>/legs' \
-H 'Content-Type: application/json' \
-d '{
"contact_uri": "+919876543210",
"exophone": "08047491899",
"leg_event_endpoint": "grpc://your-event-server.example.com",
"timeout": 30
}'
Exotel emits: leg_connecting โ leg_ringing โ leg_answered
Step 2 โ Start stream on leg_answeredโ
curl -X POST \
'https://<api_key>:<api_token>@api.in.exotel.com/v2/accounts/<account_sid>/legs/<leg_sid>/actions/start_stream' \
-H 'Content-Type: application/json' \
-d '{
"direction": "bidirectional",
"url": "wss://bot.example.com/stream",
"content_type": "audio/x-mulaw;rate=8000"
}'
Optional โ Play greeting while stream initialisesโ
Fire both requests simultaneously on leg_answered to eliminate dead air:
# Start say in parallel with start_stream
curl -X POST \
'.../legs/<leg_sid>/actions/start_say' \
-H 'Content-Type: application/json' \
-d '{ "text": "Please hold.", "loop": 0 }'
Stop it as soon as stream_started fires:
curl -X POST '.../legs/<leg_sid>/actions/stop_say'
| Scenario | Use greeting? |
|---|---|
| Inbound IVR / tech support | No |
| Outbound sales / collections | Yes |
Applet configurationโ
Stream Applet โ Unidirectional (Exotel โ your server)โ
Audio flows one way only โ from the caller to your server. Your server cannot send audio back. Use for transcription, agent assist, call monitoring.
| # | Parameter | Required | Description |
|---|---|---|---|
| 1 | Action | Yes | Start to begin streaming ยท Stop to end it |
| 2 | URL | Yes | wss://your-server.com/stream โ or https:// endpoint returning {"url":"wss://..."} |
| 3 | Next Applet | No | Call flow continues to next applet immediately after stream creation |
Unidirectional streams fork audio immediately. If used with a Connect applet that rings multiple agents, audio from all ringing legs is sent โ filter on your end.
VoiceBot Applet โ Bidirectional (Exotel โ your server)โ
Audio flows both ways โ your server receives caller audio and can send audio back in real time. Use for conversational AI, IVR replacement, outbound bots.
| # | Parameter | Required | Description |
|---|---|---|---|
| 1 | URL | Yes | wss://bot.example.com/stream โ or https:// endpoint returning {"url":"wss://..."} for dynamic routing |
| 2 | Authentication | No | IP whitelist or Basic auth header. Email hello@exotel.com for Exotel's IP ranges. |
| 3 | Sample Rate | No | 8000 (default) ยท 16000 (recommended) ยท 24000. Append ?sample-rate=16000 to URL. |
| 4 | Custom Parameters | No | Up to 3 key-value pairs in URL, max 256 chars total. Arrive in the start event. |
| 5 | Record | No | Generates recording URL in the Passthru applet |
| 6 | Next Applet | No | Stream closes automatically โ no Stop applet needed |
WebSocket protocolโ
Event support by applet typeโ
| Event | Stream Applet (Unidirectional) | VoiceBot Applet (Bidirectional) |
|---|---|---|
connected | โ | โ |
start | โ | โ |
media (receive) | โ | โ |
dtmf (receive) | โ | โ |
mark (receive) | โ | โ |
stop | โ | โ |
media (send) | โ | โ |
mark (send) | โ | โ |
clear (send) | โ | โ |
Events โ Exotel โ your serverโ
connectedโ
{ "event": "connected" }
startโ
{
"event": "start",
"sequence_number": "1",
"stream_sid": "MZxxxxxxxxxxxxxxxxxxxxxxxx",
"start": {
"stream_sid": "MZxxxxxxxxxxxxxxxxxxxxxxxx",
"call_sid": "CAxxxxxxxxxxxxxxxxxxxxxxxx",
"account_sid": "ACxxxxxxxxxxxxxxxxxxxxxxxx",
"from": "+919876543210",
"to": "+918047491899",
"custom_parameters": { "key1": "value1" },
"media_format": {
"encoding": "audio/x-raw",
"sample_rate": "8000",
"bit_rate": "16"
}
}
}
mediaโ
{
"event": "media",
"sequence_number": "3",
"stream_sid": "MZxxxxxxxxxxxxxxxxxxxxxxxx",
"media": {
"chunk": "2",
"timestamp": "200",
"payload": "<base64-encoded PCM>"
}
}
dtmf (VoiceBot Applet only)โ
{
"event": "dtmf",
"sequence_number": "7",
"stream_sid": "MZxxxxxxxxxxxxxxxxxxxxxxxx",
"dtmf": { "digit": "5", "duration": "100" }
}
mark (VoiceBot Applet only)โ
Sent when audio you previously sent has finished playing.
{
"event": "mark",
"sequence_number": "15",
"stream_sid": "MZxxxxxxxxxxxxxxxxxxxxxxxx",
"mark": { "name": "my-label" }
}
stopโ
{
"event": "stop",
"sequence_number": "20",
"stream_sid": "MZxxxxxxxxxxxxxxxxxxxxxxxx",
"stop": {
"call_sid": "CAxxxxxxxxxxxxxxxxxxxxxxxx",
"account_sid": "ACxxxxxxxxxxxxxxxxxxxxxxxx",
"reason": "callended"
}
}
reason: stopped (applet ended) ยท callended (caller hung up)
Events โ your server โ Exotel (VoiceBot Applet only)โ
These events only apply to the VoiceBot Applet (bidirectional). The Stream Applet is receive-only โ your server cannot send any events back.
media โ send audio to callerโ
{
"event": "media",
"stream_sid": "MZxxxxxxxxxxxxxxxxxxxxxxxx",
"media": { "payload": "<base64-encoded PCM>" }
}
mark โ tag a playback positionโ
{
"event": "mark",
"stream_sid": "MZxxxxxxxxxxxxxxxxxxxxxxxx",
"mark": { "name": "turn-3-end" }
}
clear โ flush buffered audio (barge-in)โ
{ "event": "clear", "stream_sid": "MZxxxxxxxxxxxxxxxxxxxxxxxx" }
Audio formatโ
| Property | Value |
|---|---|
| Codec | Raw PCM (linear16) โ uncompressed |
| Bit depth | 16-bit signed, little-endian |
| Channels | Mono |
| Default sample rate | 8 000 Hz |
| Supported rates | 8 000 ยท 16 000 ยท 24 000 Hz |
| Transport | Base64 |
| Chunk size | 3 200โ100 000 bytes, must be a multiple of 320 |
| Max session duration | 60 minutes |
Passthru Appletโ
Place immediately after the Voicebot/Stream applet. Exotel sends a GET request with stream metadata as query parameters to your callback URL when the stream ends.
| Field | Description |
|---|---|
callsid | Parent call ID |
streamsid | Stream session ID |
streamurl | WebSocket URL used |
status | Final stream status |
duration | Stream duration in seconds |
recordingurl | Recording link (if enabled) |
error | Error detail โ copy as-is for support |
disposition | Outcome classification |
disconnectedby | caller ยท bot ยท system |
detailedstatus | Fine-grained status |
Active Stream Monitoringโ
Check how many streams are live on your account.
curl -X GET \
'https://<api_key>:<api_token>@api.in.exotel.com/v1/accounts/<account_sid>/activestreams'
{
"status": "success",
"active_streams": 12,
"max_allowed_streams": 100,
"account_sid": "<account_sid>"
}
WSS error codesโ
| Code | Meaning | Common cause |
|---|---|---|
1000 | Normal closure | Clean disconnect |
1001 | Endpoint going away | Server restart |
1002โ1003 | Protocol / data error | Malformed frames |
1006 | Abnormal closure | Network drop ยท TLS failure ยท server crash |
1007โ1009 | Payload / policy / size | Chunk out of bounds |
1011 | Server error | Unhandled exception |
1012โ1013 | Restart / retry | Transient โ retry |
1006 checklist: WSS URL reachable โ TLS cert valid โ firewall allows Exotel IPs โ handshake completes within 10 s.
Echo server (VoiceBot Applet / bidirectional only)โ
import asyncio, json, websockets
async def handle(ws):
stream_sid = None
async for msg in ws:
ev = json.loads(msg)
match ev["event"]:
case "start":
stream_sid = ev["start"]["stream_sid"]
case "media":
await ws.send(json.dumps({
"event": "media",
"stream_sid": stream_sid,
"media": {"payload": ev["media"]["payload"]}
}))
case "stop":
break
async def main():
async with websockets.serve(handle, "0.0.0.0", 5001):
await asyncio.Future()
asyncio.run(main())
pip install websockets && python echo.py
ngrok http 5001 # use the https:// URL as streamurl
Relatedโ
- Stream & Voicebot Applet โ Full applet and event reference
- Bot Stream with Legs API โ Legs API reference
- Passthru Applet โ Passthru configuration