# Voice callbacks

Voice callbacks are HTTP POST requests that Sinch sends to your function when something happens during a phone call. Your function responds with [SVAML](/docs/functions/functions/concepts/svaml) to control what happens next.

There are four events. They describe the lifecycle of a call.

## Lifecycle

### Inbound call with a menu

```mermaid
sequenceDiagram
    autonumber
    participant Caller
    participant Sinch as Sinch Voice Platform
    participant Fn as Your Function

    Caller->>Sinch: Dial Sinch number
    Sinch->>Fn: ICE callback
    Fn-->>Sinch: SVAML (say + runMenu)
    Sinch->>Caller: Speak prompt
    Caller->>Sinch: Press DTMF digit
    Sinch->>Fn: PIE callback
    Fn-->>Sinch: SVAML (connectPstn)
    Sinch->>Caller: Connect the call
    Note over Caller,Sinch: Call in progress
    Caller->>Sinch: Hang up
    Sinch->>Fn: DICE callback
    Fn-->>Sinch: (informational, no response)
```

### Outbound call with AMD

```mermaid
sequenceDiagram
    autonumber
    participant You as Your code
    participant Sinch as Sinch Voice Platform
    participant Callee
    participant Fn as Your Function

    You->>Sinch: context.voice.callouts.custom(...)
    Sinch->>Callee: Ring
    Callee->>Sinch: Answer
    Sinch->>Fn: ACE callback (with AMD result)
    Fn-->>Sinch: continue (if human) or hangup (if machine)
    Note over Callee,Sinch: Call in progress
    Callee->>Sinch: Hang up
    Sinch->>Fn: DICE callback
```

Most functions only implement `ice`. You only need `pie` if you run menus, `ace` if you bridge or make outbound calls, and `dice` if you want to log or clean up when calls end.

## ICE — Incoming Call Event

Fires when a phone call reaches one of your Sinch phone numbers. This is where most of your work happens — greet the caller, run a menu, connect them somewhere, start a recording, etc.

### What you get

| Field | What it is |
|  --- | --- |
| `callid` | Unique call ID. Use this as a cache key for call state. |
| `cli` | Caller's phone number (E.164 format). |
| `to.endpoint` | The Sinch number they dialed. |
| `to.type` | `'number'`, `'username'`, or `'sip'`. |
| `domain` | `'pstn'`, `'mxp'`, or `'sip'`. |
| `originationType` | Origin type — usually `'pstn'` for phone calls. |
| `custom` | Custom data, if the call was initiated with any. |
| `timestamp` | ISO 8601 timestamp. |


### What you return

A SVAML response. Most common patterns:

```typescript
// Node.js
return createIceBuilder()
  .say('Welcome to Acme Corp.')
  .runMenu(MenuTemplates.business('Acme Corp'))
  .build();
```

```csharp
// C#
return Ok(new IceSvamletBuilder()
    .Instructions.Say("Welcome to Acme Corp.")
    .Action.RunMenu(MenuTemplates.Business("Acme Corp"))
    .Build());
```

## ACE — Answered Call Event

Fires when a call **you initiated** is answered by the other side. This is relevant for outbound calling or when your ICE response bridged the call somewhere with `connectPstn` + `enableAce: true`.

### What you get

The call ID plus an optional AMD (Answering Machine Detection) result if it was enabled.

| Field | What it is |
|  --- | --- |
| `callid` | Same call ID as the ICE that started this call. |
| `amd.status` | `'human'` or `'machine'` if AMD was enabled. |
| `amd.reason` | Why AMD chose that status. |
| `amd.duration` | How long AMD took to decide (milliseconds). |


### What you return

Usually `continue` (let the call proceed) or `hangup`.

```typescript
async ace(context, data) {
  if (data.amd?.status === 'machine') {
    return createAceBuilder().hangup().build();
  }
  return createAceBuilder()
    .say('Hello! You have a message from Acme Corp.')
    .continue()
    .build();
}
```

```csharp
public override async Task<IActionResult> Ace(AceCallbackModel data)
{
    if (data.Amd?.Status == "machine")
        return Ok(new AceSvamletBuilder().Action.Hangup().Build());

    return Ok(new AceSvamletBuilder()
        .Instructions.Say("Hello! You have a message from Acme Corp.")
        .Action.Continue()
        .Build());
}
```

## PIE — Prompt Input Event

Fires when a menu you started with `runMenu` completes — the caller pressed a key, the menu timed out, or they hung up during it.

### What you get

The call ID plus a `menuResult` describing what happened.

| Field | What it is |
|  --- | --- |
| `menuResult.menuId` | ID of the menu that completed (you set this in `runMenu`). |
| `menuResult.type` | `'return'`, `'sequence'`, `'timeout'`, `'hangup'`, `'invalidinput'` |
| `menuResult.value` | The option's action value, or the entered digit sequence. |
| `menuResult.inputMethod` | `'dtmf'` or `'voice'`. |


### Result types

| Type | Meaning |
|  --- | --- |
| `return` | Caller selected an option. `value` is the option's action value. |
| `sequence` | Caller entered multi-digit input. `value` is the digit string. |
| `timeout` | Caller didn't respond in time. |
| `hangup` | Caller hung up during the menu. |
| `invalidinput` | Caller pressed a key not mapped to any option. |


### What you return

Another SVAML response. You can run another menu, connect the call, say a message, or hang up.

```typescript
async pie(context, data) {
  const result = data.menuResult;

  if (result?.type === 'timeout' || result?.type === 'hangup') {
    return createPieBuilder().say('Goodbye!').hangup().build();
  }

  switch (result?.value) {
    case 'sales':
      return createPieBuilder().say('Connecting you to sales.').connectPstn('+15551111111').build();
    case 'support':
      return createPieBuilder().say('Connecting you to support.').connectPstn('+15552222222').build();
    default:
      return createPieBuilder().say('Invalid selection. Please try again.').continue().build();
  }
}
```

## DICE — Disconnected Call Event

Fires when the call ends. Informational only — there is nothing to control at this point, so you just log, clean up cached state, bill, or update analytics.

### What you get

| Field | What it is |
|  --- | --- |
| `callid` | Call ID. |
| `duration` | Call duration in seconds. |
| `reason` | Why the call ended — see below. |
| `result` | Call result — see below. |
| `from` | Originating number. |
| `to.endpoint` | Destination. |


### Reason values

`'N/A'`, `'TIMEOUT'`, `'CALLERHANGUP'`, `'CALLEEHANGUP'`, `'BLOCKED'`, `'NOCREDITPARTNER'`, `'GENERALERROR'`, `'CANCEL'`

### Result values

`'N/A'`, `'ANSWERED'`, `'BUSY'`, `'NOANSWER'`, `'FAILED'`

### What you return

Nothing meaningful. Node functions return `void`, C# returns `Ok()`.

```typescript
async dice(context, data) {
  const cli = await context.cache.get<string>(`call:${data.callid}:cli`);
  console.log(`Call from ${cli} ended after ${data.duration}s (${data.result})`);
  await context.cache.delete(`call:${data.callid}:cli`);
}
```

## A complete example

A voice IVR that greets the caller, runs a menu, routes them to a department, and logs the call on disconnect.

```typescript
// Node.js
import type { VoiceFunction } from '@sinch/functions-runtime';
import {
  createIceBuilder,
  createPieBuilder,
  createAceBuilder,
  MenuTemplates,
} from '@sinch/functions-runtime';

export default {
  async ice(context, data) {
    await context.cache.set(`call:${data.callid}:cli`, data.cli ?? 'unknown', 3600);
    return createIceBuilder()
      .say('Thank you for calling Acme Corp.')
      .runMenu(MenuTemplates.business('Acme Corp'))
      .build();
  },

  async pie(context, data) {
    const { type, value } = data.menuResult ?? {};
    if (type === 'timeout' || type === 'hangup') {
      return createPieBuilder().say('Goodbye!').hangup().build();
    }
    const dest = { sales: '+15551111111', support: '+15552222222' }[value ?? ''];
    return dest
      ? createPieBuilder().say(`Connecting you to ${value}.`).connectPstn(dest).build()
      : createPieBuilder().say('Invalid selection.').continue().build();
  },

  async ace(context, data) {
    return data.amd?.status === 'machine'
      ? createAceBuilder().hangup().build()
      : createAceBuilder().continue().build();
  },

  async dice(context, data) {
    const cli = await context.cache.get<string>(`call:${data.callid}:cli`);
    console.log(`Call from ${cli} ended (${data.result}, ${data.duration}s)`);
    await context.cache.delete(`call:${data.callid}:cli`);
  },
} satisfies VoiceFunction;
```

## Notify events

Besides the four callbacks above, Sinch also sends `notify` events for administrative things (recording notifications, etc.). You handle them the same way — export a `notify` function (Node) or override `Notify` (C#).

## Webhook protection

Voice callbacks can be signed by Sinch and validated by your function. Controlled by the `WEBHOOK_PROTECTION` environment variable:

| Mode | Behavior |
|  --- | --- |
| `'never'` | No validation. Development only. |
| `'deploy'` | Validate in production, skip in development. **Default.** |
| `'always'` | Always validate. |


In C# it is also exposed as `Security:WebhookProtection` in configuration.

**Legacy alias.** `PROTECT_VOICE_CALLBACKS` (and `Security:ProtectVoiceCallbacks` in C#) still work as fallback names for compatibility with older functions. Prefer the canonical names above for new code.

## Related

- [SVAML](/docs/functions/functions/concepts/svaml) — the JSON format voice handlers return
- [SVAML cheat sheet](/docs/functions/reference/svaml-cheatsheet) — the most-used actions
- [Sinch Voice Callbacks spec](/docs/voice/api-reference/callbacks/) — full field reference on developers.sinch.com
- [Handlers](/docs/functions/functions/concepts/handlers) — how Sinch maps URLs to your handlers