# 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](/docs/voice/api-reference/callbacks/) and [SVAML](/docs/voice/api-reference/svaml/) on developers.sinch.com for the full Voice API specification.

## Create from template

```bash
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

Node.js
```typescript
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;
```

C#
```csharp
using Microsoft.AspNetCore.Mvc;
using SinchFunctions.Models;
using SinchFunctions.Utils;

namespace SinchFunctions
{
    public class FunctionController : SinchVoiceController
    {
        public FunctionController(
            FunctionContext context,
            IConfiguration configuration,
            ILogger<FunctionController> logger)
            : base(context, configuration, logger) { }

        public override async Task<IActionResult> Ice([FromBody] IceCallbackModel callbackData)
        {
            var companyName = Configuration["COMPANY_NAME"] ?? "Acme Corp";

            var menu = MenuFactory.CreateMenu()
                .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();

            var result = new IceSvamletBuilder()
                .Instructions.Say($"Welcome to {companyName}.")
                .Action.RunMenu(menu)
                .Build();

            return Ok(result);
        }

        public override async Task<IActionResult> Pie([FromBody] PieCallbackModel callbackData)
        {
            var selection = callbackData.MenuResult?.Value;

            return selection switch
            {
                "sales" => Ok(new PieSvamletBuilder()
                    .Instructions.Say("Connecting you to sales now.")
                    .Action.ConnectPstn("+15551110001", cli: "+15550000000")
                    .Build()),

                "support" => Ok(new PieSvamletBuilder()
                    .Instructions.Say("Connecting you to support.")
                    .Action.ConnectPstn("+15551110002")
                    .Build()),

                "operator" => Ok(new PieSvamletBuilder()
                    .Instructions.Say("Please hold for an operator.")
                    .Action.ConnectPstn("+15551110000")
                    .Build()),

                _ => Ok(new PieSvamletBuilder()
                    .Instructions.Say("Invalid selection.")
                    .Action.Hangup()
                    .Build()),
            };
        }

        public override async Task<IActionResult> Dice([FromBody] DiceCallbackModel callbackData)
        {
            Logger.LogInformation("Call ended: Reason={Reason}, Duration={Duration}s",
                callbackData.Reason, callbackData.Duration);
            return Ok();
        }
    }
}
```

## MenuBuilder reference

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

## Multi-level menus

Use `.addSubmenu(id)` to chain submenus into a single `runMenu` action.

Node.js
```typescript
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();
```

C#
```csharp
// C# requires building each menu separately
var mainMenu = MenuFactory.CreateMenu()
    .Prompt("Press 1 for English, 2 for Spanish.")
    .Option("1", "menu(english)")
    .Option("2", "menu(spanish)")
    .Build();

var englishMenu = MenuFactory.CreateMenu()
    .Prompt("Press 1 for sales, 2 for support.")
    .Option("1", "return(en-sales)")
    .Option("2", "return(en-support)")
    .Build();
englishMenu.Id = "english";

var spanishMenu = MenuFactory.CreateMenu()
    .Prompt("Presione 1 para ventas, 2 para soporte.")
    .Option("1", "return(es-sales)")
    .Option("2", "return(es-support)")
    .Build();
spanishMenu.Id = "spanish";

var result = new IceSvamletBuilder()
    .Action.RunMenu(mainMenu)
    .Build();

return Ok(result);
```

## Pre-built MenuTemplates

Node.js
```typescript
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();
```

C#
```csharp
using SinchFunctions.Utils;

var menu = MenuTemplates.Business("Acme Corp");
var yesNo = MenuTemplates.YesNo("Do you want to continue?");
var lang = MenuTemplates.Language(new List<LanguageOption>
{
    new() { Dtmf = "1", Name = "English", Value = "en-US" },
    new() { Dtmf = "2", Name = "Spanish", Value = "es-ES" },
});
var afterHours = MenuTemplates.AfterHours("Acme Corp", "9 AM to 5 PM, Monday through Friday");

var result = new IceSvamletBuilder()
    .Action.RunMenu(menu)
    .Build();
return Ok(result);
```

| 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` |


## SVAML builder quick reference

### ICE builder

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


### PIE builder

Same instructions as ICE, plus `.connectPstn()`, `.runMenu()`, and `.hangup()`. Does not support `.park()`.

For sinch.json configuration and environment variables, see [Templates](/docs/functions/functions/templates) and [Configuration & Secrets](/docs/functions/functions/concepts/configuration-secrets).