- Caller dials your Sinch number.
- Sinch sends an ICE callback to your function.
- Your function responds with SVAML that runs a menu.
- Caller presses a key. Sinch sends a PIE callback with the selection.
- Your function responds with SVAML that connects, plays a message, or runs another menu.
- When the call ends, Sinch sends a DICE callback for logging.
See Voice Callbacks and SVAML on developers.sinch.com for the full Voice API specification.
sinch functions init simple-voice-ivr --name my-ivr
cd my-ivr
sinch functions devFor C#: sinch functions init simple-voice-ivr --name my-ivr --runtime csharp
import type { VoiceFunction, FunctionContext } from '@sinch/functions-runtime';
import type { Voice } from '@sinch/voice';
import {
createIceBuilder,
createPieBuilder,
MenuBuilder,
MenuTemplates,
createUniversalConfig,
} from '@sinch/functions-runtime';
export default {
async ice(context: FunctionContext, event: Voice.IceRequest) {
const config = createUniversalConfig(context);
const companyName = config.getVariable('COMPANY_NAME', 'Acme Corp');
const menu = new MenuBuilder()
.prompt(
`Thank you for calling ${companyName}. Press 1 for sales, 2 for support, or 0 for an operator.`
)
.repeatPrompt('Press 1 for sales, 2 for support, or 0 for an operator.')
.option('1', 'return(sales)')
.option('2', 'return(support)')
.option('0', 'return(operator)')
.timeout(8000)
.repeats(2)
.maxDigits(1)
.build();
return createIceBuilder().say(`Welcome to ${companyName}.`).runMenu(menu).build();
},
async pie(context: FunctionContext, event: Voice.PieRequest) {
const selection = event.menuResult?.value;
switch (selection) {
case 'sales':
return createPieBuilder()
.say('Connecting you to sales now.')
.connectPstn('+15551110001', { cli: '+15550000000' })
.build();
case 'support':
return createPieBuilder()
.say('Connecting you to support.')
.connectPstn('+15551110002')
.build();
case 'operator':
return createPieBuilder()
.say('Please hold for an operator.')
.connectPstn('+15551110000')
.build();
default:
const retryMenu = new MenuBuilder()
.prompt('Sorry, we did not receive your selection. Press 1 for sales, or 2 for support.')
.option('1', 'return(sales)')
.option('2', 'return(support)')
.repeats(1)
.build();
return createPieBuilder().say('Invalid selection.').runMenu(retryMenu).build();
}
},
async dice(context: FunctionContext, event: Voice.DiceRequest) {
console.log('Call ended', { reason: event.reason, duration: event.duration });
},
} satisfies VoiceFunction;| Method | Description |
|---|---|
.prompt(text) | Main prompt, spoken when the menu starts |
.repeatPrompt(text) | Spoken when the caller does not respond |
.option(dtmf, action) | Add a key mapping |
.timeout(ms) | Wait time before repeating (default 8000) |
.repeats(n) | Times to repeat before giving up (default 2) |
.maxDigits(n) | Digits to collect before triggering PIE (default 1) |
.barge(bool) | Allow keypresses to interrupt the prompt (default true) |
.build() | Returns the MenuStructure to pass to runMenu() |
Valid action formats: 'return' (raw digit), 'return(value)' (custom value), 'menu(subMenuId)' (navigate to submenu).
Use .addSubmenu(id) to chain submenus into a single runMenu action.
import { createIceBuilder, MenuBuilder } from '@sinch/functions-runtime';
const menu = new MenuBuilder()
.prompt('Press 1 for English, 2 for Spanish.')
.option('1', 'menu(english)')
.option('2', 'menu(spanish)')
.addSubmenu('english')
.prompt('Press 1 for sales, 2 for support.')
.option('1', 'return(en-sales)')
.option('2', 'return(en-support)')
.addSubmenu('spanish')
.prompt('Presione 1 para ventas, 2 para soporte.')
.option('1', 'return(es-sales)')
.option('2', 'return(es-support)')
.build();
return createIceBuilder().runMenu(menu).build();import { MenuTemplates, createIceBuilder } from '@sinch/functions-runtime';
const menu = MenuTemplates.business('Acme Corp');
const yesNo = MenuTemplates.yesNo('Do you want to continue?');
const lang = MenuTemplates.language([
{ dtmf: '1', name: 'English', value: 'en-US' },
{ dtmf: '2', name: 'Spanish', value: 'es-ES' },
]);
const afterHours = MenuTemplates.afterHours('Acme Corp', '9 AM to 5 PM, Monday through Friday');
return createIceBuilder().runMenu(menu).build();| Template | Node.js | C# | Returns |
|---|---|---|---|
| Business menu | MenuTemplates.business(name) | MenuTemplates.Business(name) | sales, support, operator |
| Yes/No | MenuTemplates.yesNo(question) | MenuTemplates.YesNo(question) | yes, no |
| Language | MenuTemplates.language(options) | MenuTemplates.Language(options) | locale value |
| After hours | MenuTemplates.afterHours(name, hours) | MenuTemplates.AfterHours(name, hours) | voicemail, website, emergency |
| Recording consent | MenuTemplates.recordingConsent() | MenuTemplates.RecordingConsent() | consent, no_consent |
| Method | Effect |
|---|---|
.say(text, locale?) | Play TTS before action |
.play(url) | Play audio file |
.answer() | Answer the call explicitly |
.setCookie(key, value) | Store state across callbacks |
.startRecording(options?) | Begin recording |
.runMenu(menu) | Run IVR menu (triggers PIE) |
.connectPstn(number, options?) | Forward to phone number |
.connectSip(sipUri, options?) | Forward to SIP endpoint |
.connectConf(conferenceId) | Join a conference |
.park(holdPrompt?) | Hold the caller |
.hangup() | End the call |
Same instructions as ICE, plus .connectPstn(), .runMenu(), and .hangup(). Does not support .park().
For sinch.json configuration and environment variables, see Templates and Configuration & Secrets.