# 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](/docs/functions/functions/concepts/svaml). For the **authoritative spec**, see [developers.sinch.com/docs/voice-2.0](https://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.

```typescript
commands().answer().say('Welcome!').hangup().build();
```

```csharp
new CommandBuilder().Answer().Say("Welcome!").Hangup().Build();
```

## `say` — text to speech

```typescript
// Node
.say('Welcome to Acme Corp!')
.say('Bonjour!', { voiceName: 'Amelie' })
```

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

```typescript
// Node
.play('https://example.com/greeting.wav')
```

```csharp
// C#
.Play("https://example.com/greeting.wav")
```

URL must be publicly reachable.

## `answer` — answer the call

```typescript
.answer()
```

```csharp
.Answer()
```

## `hangup`

```typescript
commands().say('Goodbye!').hangup().build();
// Or end a named leg instead of the current one
.hangup('destination')
```

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

```typescript
// 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)
```

```csharp
// 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):

```typescript
.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
})
```

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

```typescript
.answer().bridgeCall('main')
```

```csharp
.Answer().BridgeCall("main")
```

## `menu` — run an IVR menu

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

```typescript
// 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();
```

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

### Menu builder reference

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

```typescript
.webhook('escalate', 'https://your-function.fn.sinch.com/escalate')
```

```csharp
.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](/docs/functions/functions/concepts/voice-callbacks).

## `startRecording` / `stopRecording`

```typescript
// Node
.startRecording({ destinationUrl: 's3://my-bucket/calls', credentials: 'accessKeyId:secretAccessKey' })
.stopRecording('main')
```

```csharp
// C#
.StartRecording(new StartRecordingOptions
{
    DestinationUrl = "s3://my-bucket/calls",
    Credentials = new RecordingCredentials { AccessKeyId = "...", SecretAccessKey = "...", Region = "eu-central-1" },
})
.StopRecording("main")
```

## `amd` — answering machine detection

```typescript
.dial('+15551234567', { onAnswer: (c) => c.amd({
  onHuman: (c2) => c2.say('Hello!'),
  onMachine: (c2) => c2.hangup(),
}) })
```

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

```typescript
.pause(500) // milliseconds
```

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


## Related

- [SVAML concept](/docs/functions/functions/concepts/svaml) — what SVAML is, why a builder
- [Voice callbacks](/docs/functions/functions/concepts/voice-callbacks) — which handlers return commands and when
- [developers.sinch.com Voice API v2](https://developers.sinch.com/docs/voice-2.0) — full specification with every option