Skip to main content

Call Control

When your voice app receives a call.received webhook, your server takes ownership of the call and controls it via the Update Call API.

Text
┌─────────┐ ┌───────────┐ ┌─────────────┐
│ Caller │ │ DialStack │ │ Your Server │
└────┬────┘ └─────┬─────┘ └──────┬──────┘
│ │ │
│ 1. Call arrives │ │
│────────────────────────▶│ │
│ │ │
│ │ 2. Webhook POST │
│ │─────────────────────────▶│
│ │ │
│ │ 3. POST /v1/calls/{id} │
│ │◀─────────────────────────│
│ │ (attach audio) │
│ │ │
│ 4. Bidirectional audio │ │
│◀───────────────────────▶│◀────────────────────────▶│
│ (WebSocket) │ (WebSocket) │
│ │ │

Actions​

Use the Update Call API to send actions. Actions are processed sequentially.

Attach Audio Stream​

Connect bidirectional audio to your WebSocket server (see WebSocket API for the message protocol):

JavaScript
await dialstack.calls.update(
callId,
{ actions: [{ type: 'attach', url: 'wss://your-server.example.com/voice/stream' }] },
{ dialstackAccount: accountId }
);

The url must use wss://, so the call's audio is encrypted in transit.

The attach action blocks until the WebSocket disconnects, then processing continues with the next action.

Transfer​

Transfer the caller to an extension, an E.164 phone number, or a SIP address:

JavaScript
await dialstack.calls.update(
callId,
{ actions: [{ type: 'transfer', target: '100' }] },
{ dialstackAccount: accountId }
);

If the transfer target answers or the caller hangs up, processing stops. If the transfer fails (no answer, busy), processing continues with the next action.

Transfer to a SIP address (e.g. an AI voice agent)​

The target can also be a sip: URI, which routes the live call to an external SIP address such as an AI voice agent. Your server provides the full address — for example, after registering the call with your agent provider and receiving an identifier to dial:

JavaScript
await dialstack.calls.update(
callId,
{ actions: [{ type: 'transfer', target: 'sip:agent-7f3c@sip.example-ai.com' }] },
{ dialstackAccount: accountId }
);

This leg is not encrypted: signaling is plain SIP and the audio is connected over plain RTP. sips: URIs and ;transport=tls aren't supported. You can append ;transport=tcp or ;transport=udp to the URI to select the signaling transport. If the call's audio must be encrypted in transit, for example because it carries protected health information, stream it with attach instead.

Combining Actions​

Chain actions for fallback behavior:

JSON
{
"actions": [
{ "type": "attach", "url": "wss://ai.example.com/voice" },
{ "type": "transfer", "target": "100" }
]
}

This connects to your AI voice assistant first. When the WebSocket disconnects (e.g., AI hands off), the call transfers to extension 100.

Replacing Actions​

Sending a new update replaces all pending actions immediately. The current action is interrupted, and processing starts from the first action in the new list.

JavaScript
// AI decides to transfer the call
await dialstack.calls.update(
callId,
{ actions: [{ type: 'transfer', target: '100' }] },
{ dialstackAccount: accountId }
);

WebSocket Audio Streaming​

When DialStack executes an attach action, it connects to your WebSocket URL and streams audio bidirectionally: μ-law at 8 kHz, in base64-encoded JSON messages. The WebSocket API reference has the audio format and every message, including the begin message that opens the session and the end message sent before DialStack closes it.

Ending the Session​

Either side can close the WebSocket to end the audio session. When it closes, DialStack continues processing with the next action (if any).

Using MediaStream (SDK)​

The SDK provides a MediaStream class that parses the WebSocket messages and emits begin, audio, close (with the close code and reason) and error events. After begin, stream.callId, stream.accountId and stream.audioFormat are set.

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

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', (ws) => {
const stream = new MediaStream(ws);

stream.on('begin', (event) => {
console.log('Call started:', event.call_id);
console.log('Audio format:', event.audio_format);

// Send greeting audio
stream.sendAudio(greetingAudioBase64);
});

stream.on('audio', (event) => {
// event.payload contains base64-encoded μ-law audio
// event.timestamp contains the audio timestamp

// Process with your AI pipeline and respond
const responseAudio = processAudio(event.payload);
stream.sendAudio(responseAudio);

// Or send raw Buffer (auto base64-encoded)
stream.sendAudioBuffer(audioBuffer);
});

stream.on('close', (code, reason) => {
console.log('Stream closed:', code, reason);
});

stream.on('error', (error) => {
console.error('Stream error:', error);
});
});

Complete Example: AI Voice Assistant​

JavaScript
import express from 'express';
import { WebSocketServer } from 'ws';
import { DialStack, MediaStream } 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
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);
}

const { call_id, account_id, from_number, from_name } = event;
console.log(`Incoming call from ${from_name || from_number}`);

res.sendStatus(200);

// Attach audio stream with fallback transfer
await dialstack.calls.update(
call_id,
{
actions: [
{ type: 'attach', url: 'wss://your-server.example.com/voice/stream' },
{ type: 'transfer', target: '100' },
],
},
{ dialstackAccount: account_id }
);
});

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

wss.on('connection', (ws) => {
const stream = new MediaStream(ws);

stream.on('begin', (event) => {
console.log(`Audio stream started for call ${stream.callId}`);
stream.sendAudio(generateGreetingAudio());
});

stream.on('audio', (event) => {
const audioBuffer = Buffer.from(event.payload, 'base64');
processAudioWithAI(audioBuffer, (responseAudio) => {
stream.sendAudio(responseAudio);
});
});

stream.on('close', () => {
console.log(`Audio stream ended for call ${stream.callId}`);
});
});

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

See also​

  • Voice Apps — creating a voice app, its webhooks, and dial plan nodes.
  • Listeners — stream one-way audio without taking over the call.
  • WebSocket API — the audio streaming protocol.
  • Update Call — the full list of actions.