Skip to content

Build a Voice IVR

How a call flows

  1. Caller dials your Sinch number.
  2. Sinch sends an ICE callback to your function.
  3. Your function responds with SVAML that runs a menu.
  4. Caller presses a key. Sinch sends a PIE callback with the selection.
  5. Your function responds with SVAML that connects, plays a message, or runs another menu.
  6. 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.

Create from template

sinch functions init simple-voice-ivr --name my-ivr
cd my-ivr
sinch functions dev

For C#: sinch functions init simple-voice-ivr --name my-ivr --runtime csharp

Complete example

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;
MethodDescription
.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).

Multi-level menus

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();

Pre-built MenuTemplates

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();
TemplateNode.jsC#Returns
Business menuMenuTemplates.business(name)MenuTemplates.Business(name)sales, support, operator
Yes/NoMenuTemplates.yesNo(question)MenuTemplates.YesNo(question)yes, no
LanguageMenuTemplates.language(options)MenuTemplates.Language(options)locale value
After hoursMenuTemplates.afterHours(name, hours)MenuTemplates.AfterHours(name, hours)voicemail, website, emergency
Recording consentMenuTemplates.recordingConsent()MenuTemplates.RecordingConsent()consent, no_consent

SVAML builder quick reference

ICE builder

MethodEffect
.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

PIE builder

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.