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:
npm install @dialstack/sdk-server
Initialize the client with your API key:
import { DialStack } from '@dialstack/sdk-server';
const dialstack = new DialStack(process.env.DIALSTACK_API_KEY);
Creating a Voice App
- SDK
- cURL
const voiceApp = await dialstack.voiceApps.create(
{ name: 'AI Receptionist', url: 'https://your-server.example.com/voice/webhook' },
{ dialstackAccount: 'acct_01h2xcejqtf2nbrexx3vqjhp41' }
);
curl -X POST https://api.dialstack.ai/v1/voice-apps \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-H "DialStack-Account: acct_01h2xcejqtf2nbrexx3vqjhp41" \
-H "Content-Type: application/json" \
-d '{
"name": "AI Receptionist",
"url": "https://your-server.example.com/voice/webhook"
}'
Response:
{
"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
| Event | Description | Trigger |
|---|---|---|
call.received | A call has been routed to this voice app for handling | Voice app is the call destination (its extension, or a control-mode node) |
call.notify | A call is passing through a Voice App (Notify) node in a dial plan | A 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
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:
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}.
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 a2xxwithin 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.
{
"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.
┌──────────┐ ┌──────────────┐ ┌──────────┐ ┌─────────┐
│ 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
nextnode 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.receivedwebhooks
{
"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)