Skip to content

SVAML cheat sheet

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.

One builder

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

say — text to speech

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

play — play audio

// Node
.play('https://example.com/greeting.wav')
// C#
.Play("https://example.com/greeting.wav")

URL must be publicly reachable.

answer — answer the call

.answer()
.Answer()

hangup

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

dialPhone/dialSip/dialStream/dialRelay — bridge a party onto the call

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)

dial — the low-level primitive

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.

bridgeCall — join a named bridge

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.

MethodDescription
.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 — name a mid-call event

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

startRecording / stopRecording

// 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")

amd — answering machine detection

.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

.pause(500) // milliseconds
.Pause(500)

Don't

  • 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 — onAnswer plus whichever others you need, or none at all. dialPhone/dialSip/dialStream/dialRelay's onNoAnswer sidesteps 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.
We'd love to hear from you!
Rate this content: