Skip to main content

Voice Apps

Build programmable voice applications with webhooks and real-time audio streaming.

Overview​

Voice apps let you handle calls programmatically. DialStack notifies your server via webhook, and you decide what happens next. Voice apps support two modes:

Call Control — Your server takes ownership of the call. Connect bidirectional audio for AI voice assistants, transfer calls to extensions, or build IVR systems. See Call Control.

Call Listening — Stream real-time audio from calls without affecting them. Use this for live monitoring, real-time transcription, or analytics. See Listeners.

Both modes start with a webhook notification to your server. The webhook's event field tells you which mode triggered it.

Installation​

Install the DialStack server SDK for Node.js:

Bash
npm install @dialstack/sdk-server

Initialize the client with your API key:

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

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

Creating a Voice App​

JavaScript
const voiceApp = await dialstack.voiceApps.create(
{ name: 'AI Receptionist', url: 'https://your-server.example.com/voice/webhook' },
{ dialstackAccount: 'acct_01h2xcejqtf2nbrexx3vqjhp41' }
);

Response:

JSON
{
"id": "va_01h2xcejqtf2nbrexx3vqjhp49",
"name": "AI Receptionist",
"url": "https://your-server.example.com/voice/webhook",
"status": "active",
"secret": "whsec_abc123def456...",
"created_at": "2025-10-18T10:00:00Z",
"updated_at": "2025-10-18T10:00:00Z"
}

Important: Save the secret value - you'll need it to verify webhook signatures. You can read it again with GET /v1/voice-apps/{voice_app_id} and replace it with POST /v1/voice-apps/{voice_app_id}/rotate_secret.

Webhook Notifications​

When a call reaches your voice app, DialStack sends an HTTP POST to your webhook URL. The same voice app can receive both event types — the event field tells you which one.

Webhook Events​

EventDescriptionTrigger
call.receivedA call has been routed to this voice app for handlingVoice app is the call destination (its extension, or a control-mode node)
call.notifyA call is passing through a Voice App (Notify) node in a dial planA notify-mode node in a dial plan references this voice app

For call.received, your server takes control of the call — use the Update Call API to attach audio, transfer, etc. For call.notify, the call continues routing normally — use the Listeners API to stream audio if desired.

Webhook Payload​

HTTP
POST /voice/webhook HTTP/1.1
Host: your-server.example.com
Content-Type: application/json
X-DialStack-Signature: t=1697634600,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

{
"event": "call.received",
"call_id": "call_01h2xcejqtf2nbrexx3vqjhp45",
"account_id": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"voice_app_id": "va_01h2xcejqtf2nbrexx3vqjhp49",
"from_number": "+14155551234",
"from_name": "John Smith",
"to_number": "+14155559876"
}

Both call.received and call.notify use the same payload shape. The event field is the only difference. from_name is an empty string when the caller's name is unknown.

Verifying Signatures​

Voice app webhooks are signed with the voice app's secret, using the same scheme as every DialStack webhook. Signature verification has the algorithm and examples in Node.js, Python and Go. With the SDK, call the static DialStack.webhooks.constructEvent:

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

const event = DialStack.webhooks.constructEvent(
req.body, // the raw request body
req.headers['x-dialstack-signature'],
process.env.VOICE_APP_SECRET
);

// event contains: event, call_id, account_id, voice_app_id, from_number, from_name, to_number

Webhook Response​

Return a 2xx to acknowledge receipt. The response body is ignored: to act on the call, call the API, for example POST /v1/calls/{call_id}.

JavaScript
app.post('/voice/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const event = DialStack.webhooks.constructEvent(
req.body,
req.headers['x-dialstack-signature'],
process.env.VOICE_APP_SECRET
);

// Acknowledge immediately
res.sendStatus(200);

// Handle based on event type
if (event.event === 'call.received') {
handleCallControl(event.call_id);
} else if (event.event === 'call.notify') {
handleCallNotify(event.call_id);
}
});
  • call.received: the caller is answered and hears hold audio while DialStack waits for your response. If your server doesn't return a 2xx within 10 seconds, the call is ended.
  • call.notify: the call is not answered or held. DialStack waits up to 3 seconds for your response, then continues routing whatever the outcome.

Voice App Dial Plan Nodes​

Voice apps are used in dial plans through the voice_app node. Its mode field selects one of two behaviors: control (the default) and notify.

Voice App (Control)​

Routes the call to the voice app. Your server receives a call.received webhook and takes ownership of the call — attaching audio, transferring, etc. The dial plan ends at this node.

JSON
{
"id": "ai_receptionist",
"type": "voice_app",
"config": {
"voice_app": "va_01h2xcejqtf2nbrexx3vqjhp49",
"mode": "control"
}
}

Voice App (Notify)​

Sends a notification to the voice app as the call passes through, without interrupting call routing. This is useful for triggering external actions (real-time transcription, call analytics, CRM logging) alongside normal call handling.

Text
┌──────────┐ ┌──────────────┐ ┌──────────┐ ┌─────────┐
│ Schedule │────▶│ Voice App │────▶│ Dial │────▶│Voicemail│
│ Node │ │ (Notify) │ │ User │ │ │
└──────────┘ └──────┬───────┘ └──────────┘ └─────────┘
│
│ POST (fire-and-forget)
▼
┌─────────────┐
│ Your Server │
└─────────────┘

The Voice App (Notify) node:

  • Sends an HTTP POST to the voice app's URL with "event": "call.notify"
  • Continues to the next node once your server responds, or after 3 seconds — a failed or slow webhook never stops the call
  • Does not answer or interrupt the call
  • Uses the same signature verification as call.received webhooks
JSON
{
"id": "notify_transcription",
"type": "voice_app",
"config": {
"voice_app": "va_01h2xcejqtf2nbrexx3vqjhp49",
"mode": "notify",
"next": "dial_reception"
}
}

A notify node must reference a plain webhook voice app. The voice app an AI agent manages always takes control of the call, so saving a dial plan with a notify node that references one returns 422.

Next steps​

  • Call Control — attach a bidirectional audio stream, transfer, and chain actions after call.received.
  • Listeners — stream one-way audio from a call after call.notify.
  • WebSocket API — the audio streaming protocol for both.

API Reference​

  • Voice Apps — Create and manage voice apps
  • Update Call — Control active calls with actions
  • Listeners — Stream real-time audio from active calls
  • WebSocket API — Audio streaming protocol (voice apps and listeners)