# Voice callbacks 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. ## Lifecycle ### Inbound call with a menu ```mermaid sequenceDiagram autonumber participant Caller participant Sinch as Sinch Voice Platform participant Fn as Your Function Caller->>Sinch: Dial Sinch number Sinch->>Fn: ICE callback Fn-->>Sinch: SVAML (say + runMenu) Sinch->>Caller: Speak prompt Caller->>Sinch: Press DTMF digit Sinch->>Fn: PIE callback Fn-->>Sinch: SVAML (connectPstn) Sinch->>Caller: Connect the call Note over Caller,Sinch: Call in progress Caller->>Sinch: Hang up Sinch->>Fn: DICE callback Fn-->>Sinch: (informational, no response) ``` ### Outbound call with AMD ```mermaid sequenceDiagram autonumber participant You as Your code participant Sinch as Sinch Voice Platform participant Callee participant Fn as Your Function You->>Sinch: context.voice.callouts.custom(...) Sinch->>Callee: Ring Callee->>Sinch: Answer Sinch->>Fn: ACE callback (with AMD result) Fn-->>Sinch: continue (if human) or hangup (if machine) Note over Callee,Sinch: Call in progress Callee->>Sinch: Hang up Sinch->>Fn: DICE callback ``` 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. ## ICE — Incoming Call Event 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. ### What you get | 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. | ### What you return A SVAML response. Most common patterns: ```typescript // Node.js return new IceSvamlBuilder() .say('Welcome to Acme Corp.') .runMenu(MenuTemplates.business('Acme Corp')) .build(); ``` ```csharp // C# return Ok(new IceSvamletBuilder() .Instructions.Say("Welcome to Acme Corp.") .Action.RunMenu(MenuTemplates.Business("Acme Corp")) .Build()); ``` ## ACE — Answered Call Event 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`. ### What you get 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). | ### What you return Usually `continue` (let the call proceed) or `hangup`. ```typescript 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(); } ``` ```csharp public override async Task 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()); } ``` ## PIE — Prompt Input Event Fires when a menu you started with `runMenu` completes — the caller pressed a key, the menu timed out, or they hung up during it. ### What you get 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'`. | ### Result types | 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. | ### What you return Another SVAML response. You can run another menu, connect the call, say a message, or hang up. ```typescript 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(); } } ``` ## DICE — Disconnected Call Event 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. ### What you get | 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. | ### Reason values `'N/A'`, `'TIMEOUT'`, `'CALLERHANGUP'`, `'CALLEEHANGUP'`, `'BLOCKED'`, `'NOCREDITPARTNER'`, `'GENERALERROR'`, `'CANCEL'` ### Result values `'N/A'`, `'ANSWERED'`, `'BUSY'`, `'NOANSWER'`, `'FAILED'` ### What you return Nothing meaningful. Node functions return `void`, C# returns `Ok()`. ```typescript async dice(context, data) { const cli = await context.cache.get(`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 complete example A voice IVR that greets the caller, runs a menu, routes them to a department, and logs the call on disconnect. ```typescript // 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(`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; ``` ## Notify events 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#). ## Webhook protection 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. ## Related - [SVAML](#) — the JSON format voice handlers return - [SVAML cheat sheet](/docs/functions/reference/svaml-cheatsheet) — the most-used actions - [Sinch Voice Callbacks spec](/docs/voice/api-reference/voice-callbacks/) — full field reference on developers.sinch.com - [Handlers](#) — how Sinch maps URLs to your handlers