Skip to main content

Listeners

When your voice app receives a call.notify webhook from a notify-mode node, you can create a listener to stream real-time audio from the call. Audio flows one way only — from DialStack to your server; the listener is passive and does not inject audio or alter the call. Neither party hears a tone or indication that a listener is attached, so you are responsible for obtaining appropriate consent from the parties on the call in accordance with applicable law (recording-consent requirements vary by jurisdiction).

Text
┌────────┐ ┌───────────┐ ┌─────────────┐
│ Caller │ │ DialStack │ │ Your Server │
└───┬────┘ └─────┬─────┘ └──────┬──────┘
│ │ │
│ Normal two-party call │ │
│◀─────────────────────────▶│ │
│ │ │
│ │ 1. Webhook (call.notify) │
│ │──────────────────────────▶│
│ │ │
│ │ 2. POST /v1/calls/{id}/ │
│ │ listeners │
│ │◀──────────────────────────│
│ │ │
│ Call continues normally │ 3. Audio (one-way WSS) │
│◀─────────────────────────▶│──────────────────────────▶│
│ │ │
  1. A Voice App (Notify) node notifies your server that a call has started
  2. Your server creates a listener on that call
  3. DialStack opens a WebSocket to your server and streams audio

Creating a Listener​

JavaScript
const listener = await dialstack.calls.createListener(
callId,
{
url: 'wss://your-server.example.com/audio',
channel: 'both',
},
{ dialstackAccount: accountId }
);

Channel Selection​

ChannelAudio received
callerAudio from the party that initiated the call
calleeAudio from the party that received the call
bothBoth channels, delivered as separate tagged messages

Stopping a Listener​

Listeners stop automatically when the call ends. To stop early:

JavaScript
await dialstack.calls.delListener(callId, listenerId, {
dialstackAccount: accountId,
});

Listener WebSocket Protocol​

A listener session uses the same WebSocket protocol as attach, one way only: DialStack sends, and does not read what you send back. Three things are specific to listeners:

  • The begin message carries listener_id instead of voice_app_id, which lets one server handle both kinds of session.
  • With channel: "both", each audio message carries a channel field, caller or callee.
  • Before closing the socket, DialStack sends an end message whose reason is call_ended, deleted (stopped with the API) or error.

Complete Example: Real-Time Transcription​

JavaScript
import express from 'express';
import { WebSocketServer } from 'ws';
import { DialStack } from '@dialstack/sdk-server';

const app = express();
const dialstack = new DialStack(process.env.DIALSTACK_API_KEY);
const VOICE_APP_SECRET = process.env.VOICE_APP_SECRET;

// Webhook endpoint — receives call.notify from Voice App (Notify) dial plan node
app.post('/voice/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
let event;
try {
event = DialStack.webhooks.constructEvent(
req.body,
req.headers['x-dialstack-signature'],
VOICE_APP_SECRET
);
} catch (err) {
return res.sendStatus(401);
}

res.sendStatus(200);

if (event.event === 'call.notify') {
// Create a listener to stream audio for transcription
await dialstack.calls.createListener(
event.call_id,
{
url: 'wss://your-server.example.com/audio',
channel: 'both',
},
{ dialstackAccount: event.account_id }
);
}
});

// WebSocket server for receiving listener audio
const wss = new WebSocketServer({ noServer: true });

wss.on('connection', (ws) => {
let listenerId;

ws.on('message', (data) => {
const message = JSON.parse(data);

switch (message.event) {
case 'begin':
listenerId = message.listener_id;
console.log(`Listening to call ${message.call_id}`);
break;

case 'audio':
// Send to your speech-to-text service
transcribe(message.payload, message.channel);
break;

case 'end':
console.log(`Listener ${listenerId} stopped: ${message.reason}`);
break;
}
});
});

const server = app.listen(3000);
server.on('upgrade', (request, socket, head) => {
if (request.url === '/audio') {
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit('connection', ws, request);
});
} else {
socket.destroy();
}
});

See also​