Voice callbacks are HTTP POST requests that Sinch sends to your function when something happens during a phone call. Your function responds with SVAML to control what happens next.
There are four events. They describe the lifecycle of a call.
Most functions only implement ice. You only need pie if you run menus, ace if you bridge or make outbound calls, and dice if you want to log or clean up when calls end.
Fires when a phone call reaches one of your Sinch phone numbers. This is where most of your work happens — greet the caller, run a menu, connect them somewhere, start a recording, etc.
| Field | What it is |
|---|---|
callid | Unique call ID. Use this as a cache key for call state. |
cli | Caller's phone number (E.164 format). |
to.endpoint | The Sinch number they dialed. |
to.type | 'number', 'username', or 'sip'. |
domain | 'pstn', 'mxp', or 'sip'. |
originationType | Origin type — usually 'pstn' for phone calls. |
custom | Custom data, if the call was initiated with any. |
timestamp | ISO 8601 timestamp. |
A SVAML response. Most common patterns:
// Node.js
return new IceSvamlBuilder()
.say('Welcome to Acme Corp.')
.runMenu(MenuTemplates.business('Acme Corp'))
.build();// C#
return Ok(new IceSvamletBuilder()
.Instructions.Say("Welcome to Acme Corp.")
.Action.RunMenu(MenuTemplates.Business("Acme Corp"))
.Build());Fires when a call you initiated is answered by the other side. This is relevant for outbound calling or when your ICE response bridged the call somewhere with connectPstn + enableAce: true.
The call ID plus an optional AMD (Answering Machine Detection) result if it was enabled.
| Field | What it is |
|---|---|
callid | Same call ID as the ICE that started this call. |
amd.status | 'human' or 'machine' if AMD was enabled. |
amd.reason | Why AMD chose that status. |
amd.duration | How long AMD took to decide (milliseconds). |
Usually continue (let the call proceed) or hangup.
async ace(context, data) {
if (data.amd?.status === 'machine') {
return new AceSvamlBuilder().hangup().build();
}
return new AceSvamlBuilder()
.say('Hello! You have a message from Acme Corp.')
.continue()
.build();
}public override async Task<IActionResult> Ace(AceCallbackModel data)
{
if (data.Amd?.Status == "machine")
return Ok(new AceSvamletBuilder().Action.Hangup().Build());
return Ok(new AceSvamletBuilder()
.Instructions.Say("Hello! You have a message from Acme Corp.")
.Action.Continue()
.Build());
}Fires when a menu you started with runMenu completes — the caller pressed a key, the menu timed out, or they hung up during it.
The call ID plus a menuResult describing what happened.
| Field | What it is |
|---|---|
menuResult.menuId | ID of the menu that completed (you set this in runMenu). |
menuResult.type | 'return', 'sequence', 'timeout', 'hangup', 'invalidinput' |
menuResult.value | The option's action value, or the entered digit sequence. |
menuResult.inputMethod | 'dtmf' or 'voice'. |
| Type | Meaning |
|---|---|
return | Caller selected an option. value is the option's action value. |
sequence | Caller entered multi-digit input. value is the digit string. |
timeout | Caller didn't respond in time. |
hangup | Caller hung up during the menu. |
invalidinput | Caller pressed a key not mapped to any option. |
Another SVAML response. You can run another menu, connect the call, say a message, or hang up.
async pie(context, data) {
const result = data.menuResult;
if (result?.type === 'timeout' || result?.type === 'hangup') {
return new PieSvamlBuilder().say('Goodbye!').hangup().build();
}
switch (result?.value) {
case 'sales':
return new PieSvamlBuilder().say('Connecting you to sales.').connectPstn('+15551111111').build();
case 'support':
return new PieSvamlBuilder().say('Connecting you to support.').connectPstn('+15552222222').build();
default:
return new PieSvamlBuilder().say('Invalid selection. Please try again.').continue().build();
}
}Fires when the call ends. Informational only — there is nothing to control at this point, so you just log, clean up cached state, bill, or update analytics.
| Field | What it is |
|---|---|
callid | Call ID. |
duration | Call duration in seconds. |
reason | Why the call ended — see below. |
result | Call result — see below. |
from | Originating number. |
to.endpoint | Destination. |
'N/A', 'TIMEOUT', 'CALLERHANGUP', 'CALLEEHANGUP', 'BLOCKED', 'NOCREDITPARTNER', 'GENERALERROR', 'CANCEL'
'N/A', 'ANSWERED', 'BUSY', 'NOANSWER', 'FAILED'
Nothing meaningful. Node functions return void, C# returns Ok().
async dice(context, data) {
const cli = await context.cache.get<string>(`call:${data.callid}:cli`);
context.log?.info(`Call from ${cli} ended after ${data.duration}s (${data.result})`);
await context.cache.delete(`call:${data.callid}:cli`);
}A voice IVR that greets the caller, runs a menu, routes them to a department, and logs the call on disconnect.
// Node.js
import type { VoiceFunction } from '@sinch/functions-runtime';
import { IceSvamlBuilder, PieSvamlBuilder, AceSvamlBuilder, MenuTemplates } from '@sinch/functions-runtime';
export default {
async ice(context, data) {
await context.cache.set(`call:${data.callid}:cli`, data.cli ?? 'unknown', 3600);
return new IceSvamlBuilder()
.say('Thank you for calling Acme Corp.')
.runMenu(MenuTemplates.business('Acme Corp'))
.build();
},
async pie(context, data) {
const { type, value } = data.menuResult ?? {};
if (type === 'timeout' || type === 'hangup') {
return new PieSvamlBuilder().say('Goodbye!').hangup().build();
}
const dest = { sales: '+15551111111', support: '+15552222222' }[value ?? ''];
return dest
? new PieSvamlBuilder().say(`Connecting you to ${value}.`).connectPstn(dest).build()
: new PieSvamlBuilder().say('Invalid selection.').continue().build();
},
async ace(context, data) {
return data.amd?.status === 'machine'
? new AceSvamlBuilder().hangup().build()
: new AceSvamlBuilder().continue().build();
},
async dice(context, data) {
const cli = await context.cache.get<string>(`call:${data.callid}:cli`);
console.log(`Call from ${cli} ended (${data.result}, ${data.duration}s)`);
await context.cache.delete(`call:${data.callid}:cli`);
},
} satisfies VoiceFunction;Besides the four callbacks above, Sinch also sends notify events for administrative things (recording notifications, etc.). You handle them the same way — export a notify function (Node) or override Notify (C#).
Voice callbacks can be signed by Sinch and validated by your function. Controlled by the WEBHOOK_PROTECTION environment variable:
| Mode | Behavior |
|---|---|
'never' | No validation. Development only. |
'deploy' | Validate in production, skip in development. Default. |
'always' | Always validate. |
In C# it is also exposed as Security:WebhookProtection in configuration.
Legacy alias. PROTECT_VOICE_CALLBACKS (and Security:ProtectVoiceCallbacks in C#) still work as fallback names for compatibility with older functions. Prefer the canonical names above for new code.
- SVAML — the JSON format voice handlers return
- SVAML cheat sheet — the most-used actions
- Sinch Voice Callbacks spec — full field reference on developers.sinch.com
- Handlers — how Sinch maps URLs to your handlers