The commands you will actually use. Node.js uses commands() from @sinch/functions-runtime/voice, or the builder onCall already primed with the call. C# uses the equivalent CommandBuilder from SinchFunctions.Voice.V2, with matching PascalCase methods.
For the concept of SVAML (what it is, when it matters), see the SVAML concept page. For the authoritative spec, see developers.sinch.com/docs/voice-2.0.
Every command chains off the same builder and ends with .build() (Node) or .Build() (C#) — there is no per-callback builder split like v1 had.
commands().answer().say('Welcome!').hangup().build();new CommandBuilder().Answer().Say("Welcome!").Hangup().Build();// Node
.say('Welcome to Acme Corp!')
.say('Bonjour!', { voiceName: 'Amelie' })// C#
.Say("Welcome to Acme Corp!")
.Say("Bonjour!", new SayOptions { VoiceName = "Amelie" })Default voice: Emma. Pass { format: 'SSML' } / new SayOptions { Format = "SSML" } to send an SSML document instead of plain text.
// Node
.play('https://example.com/greeting.wav')// C#
.Play("https://example.com/greeting.wav")URL must be publicly reachable.
.answer().Answer()commands().say('Goodbye!').hangup().build();
// Or end a named leg instead of the current one
.hangup('destination')new CommandBuilder().Say("Goodbye!").Hangup().Build();
.Hangup("destination")The everyday way to connect a second party: each bridges a new leg onto the live call and tears it down when either side hangs up, filling in the leg names, the bridge, and (for phone/SIP) from for you.
// Node — simple
.dialPhone('+15551234567')
// Node — with options
.dialPhone('+15551234567', {
from: '+15559876543', // caller identity presented to the callee
timeout: 30, // seconds to wait for an answer, max 60
maxDuration: 3600, // seconds, max 14400
onNoAnswer: (c) => c.say('Nobody is free.').hangup(), // busy, rejected, timed-out and failed at once
})
// SIP, stream, or Voice Relay
.dialSip('sip:alice@sip.example.com', { transport: 'TLS' })
.dialStream('/media')
.dialRelay(relay)// C# — simple
.DialPhone("+15551234567")
// C# — with options
.DialPhone("+15551234567", new BridgeToOptions {
From = "+15559876543",
Timeout = 30,
MaxDuration = 3600,
OnNoAnswer = new CommandBuilder().Say("Nobody is free.").Hangup(),
})
// SIP, stream, or Voice Relay
.DialSip("sip:alice@sip.example.com", new DialSipOptions { Transport = SipTransport.Tls })
.DialStream("/media")
.DialRelay(relay)dialPhone/dialSip/dialStream/dialRelay are built on this — reach for it directly only when you need to hand-build the bridge and teardown yourself (or a destination none of the helpers cover):
.dial('+15551234567', {
from: '+15559876543',
callName: 'destination', // name this leg, referenced by endOnHangup/endOnFailure
timeout: 30,
maxDuration: 3600,
endOnHangup: 'origin', // leg to end when this one hangs up after connecting
endOnFailure: 'origin', // leg to end when this one never connects
onAnswer: (c) => c.say('Connected.'), // required if any other event below is set
}).Dial("+15551234567", new DialOptions {
From = "+15559876543",
CallName = "destination",
Timeout = 30,
MaxDuration = 3600,
EndOnHangup = "origin",
EndOnFailure = "origin",
})Naming any of onAnswer/onBusy/onReject/onTimeout/onHangup/onFailure switches the platform out of service-webhook mode for all six — always include onAnswer once you set any of the others, or the leg answers into silence.
Connects the current leg onto a bridge so a second, separately dialled leg can join it and the two hear each other.
.answer().bridgeCall('main').Answer().BridgeCall("main")dialPhone inside a match needs a live leg to bridge to, so a menu like this one runs on the builder a handler receives (incoming: (call, builder) => builder...), not a free-standing commands()/new CommandBuilder() — shown bare below only to keep the snippet focused on the menu syntax itself.
// Node
commands()
.answer()
.menu('main', (menu) =>
menu
.prompt('Press 1 for sales, press 2 for support.')
.repeatPrompt('Press 1 for sales, or 2 for support.')
.inputTimeout(5)
.repeatCount(2)
.maxLength(1)
.match('1', (c) => c.say('Connecting you to sales.').dialPhone('+15551111111'))
.match('2', (c) => c.say('Connecting you to support.').dialPhone('+15552222222'))
.onFail((c) => c.say('No input received. Goodbye.').hangup())
)
.build();// C#
new CommandBuilder()
.Answer()
.Menu("main", menu => menu
.Prompt("Press 1 for sales, press 2 for support.")
.RepeatPrompt("Press 1 for sales, or 2 for support.")
.InputTimeout(5)
.RepeatCount(2)
.MaxLength(1)
.Match("1", flow => flow.Say("Connecting you to sales.").DialPhone("+15551111111"))
.Match("2", flow => flow.Say("Connecting you to support.").DialPhone("+15552222222"))
.OnFail(flow => flow.Say("No input received. Goodbye.").Hangup()))
.Build();A menu with a match()/onFail() on every branch resolves on the platform — the function never sees a call.menu event for it. Drop match()/onFail() to route the digits to the manage handler instead.
| Method | Description |
|---|---|
.prompt(text) | Main prompt, spoken when the menu starts |
.repeatPrompt(text) | Spoken when the caller does not respond |
.inputTimeout(seconds) | Seconds of silence that count as no input |
.repeatCount(times) | Times to repeat the prompt before giving up |
.minLength(count) | Minimum digits to collect |
.maxLength(count) | Maximum digits to collect |
.terminatedBy(sequence) | Digits/*/# that end input early |
.match(pattern, flow) | Commands to run when the input matches pattern |
.onFail(flow) | Commands to run when nothing matches, or input times out |
.gotoMenu(name) | (On the outer builder) jump into another named menu |
.webhook('escalate', 'https://your-function.fn.sinch.com/escalate').Webhook("escalate", "https://your-function.fn.sinch.com/escalate")Delivered to your function as call.webhook.escalate. Register a handler for the name under onCall's webhooks map (Node) or override OnWebhook/OnWebhookAsync (C#) — see voice callbacks.
// Node
.startRecording({ destinationUrl: 's3://my-bucket/calls', credentials: 'accessKeyId:secretAccessKey' })
.stopRecording('main')// C#
.StartRecording(new StartRecordingOptions
{
DestinationUrl = "s3://my-bucket/calls",
Credentials = new RecordingCredentials { AccessKeyId = "...", SecretAccessKey = "...", Region = "eu-central-1" },
})
.StopRecording("main").dial('+15551234567', { onAnswer: (c) => c.amd({
onHuman: (c2) => c2.say('Hello!'),
onMachine: (c2) => c2.hangup(),
}) }).Dial("+15551234567", new DialOptions {
OnAnswer = new CommandBuilder().Amd(new AmdOptions {
OnHuman = new CommandBuilder().Say("Hello!"),
OnMachine = new CommandBuilder().Hangup(),
}),
})amd only makes sense on the low-level dial (it needs to run before deciding whether to bridge at all) — see the "name one event, name them all" rule above.
.pause(500) // milliseconds.Pause(500)- Don't return raw JSON objects from
voiceWebhook— always.build()through the command builder. - Don't add commands after
.build()— the builder is frozen. - Don't name only some of the low-level
dial's outcome events —onAnswerplus whichever others you need, or none at all.dialPhone/dialSip/dialStream/dialRelay'sonNoAnswersidesteps this by covering busy/rejected/timed-out/failed together. - Don't reach for the v1
Ice*/Pie*/Ace*builders in a new function — they exist only for functions still wired to a v1 Voice application.
- SVAML concept — what SVAML is, why a builder
- Voice callbacks — which handlers return commands and when
- developers.sinch.com Voice API v2 — full specification with every option



