Skip to content

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 to control what happens next.

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

Lifecycle

Inbound call with a menu

Your FunctionSinch Voice PlatformCallerYour FunctionSinch Voice PlatformCallerCall in progressDial Sinch number1ICE callback2SVAML (say + runMenu)3Speak prompt4Press DTMF digit5PIE callback6SVAML (connectPstn)7Connect the call8Hang up9DICE callback10(informational, no response)11

Outbound call with AMD

Your FunctionCalleeSinch Voice PlatformYour codeYour FunctionCalleeSinch Voice PlatformYour codeCall in progresscontext.voice.callouts.custom(...)1Ring2Answer3ACE callback (with AMD result)4continue (if human) or hangup (if machine)5Hang up6DICE callback7

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

FieldWhat it is
callidUnique call ID. Use this as a cache key for call state.
cliCaller's phone number (E.164 format).
to.endpointThe Sinch number they dialed.
to.type'number', 'username', or 'sip'.
domain'pstn', 'mxp', or 'sip'.
originationTypeOrigin type — usually 'pstn' for phone calls.
customCustom data, if the call was initiated with any.
timestampISO 8601 timestamp.

What you return

A SVAML response. Most common patterns:

// Node.js
return new IceSvamlBuilder()
  .say('Welcome to Acme Corp.')
  .runMenu(MenuTemplates.business('Acme Corp'))
  .build();
// 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.

FieldWhat it is
callidSame call ID as the ICE that started this call.
amd.status'human' or 'machine' if AMD was enabled.
amd.reasonWhy AMD chose that status.
amd.durationHow long AMD took to decide (milliseconds).

What you return

Usually continue (let the call proceed) or hangup.

async ace(context, data) {
  if (data.amd?.status === 'machine') {
    return new AceSvamlBuilder().hangup().build();
  }
  return new AceSvamlBuilder()
    .say('Hello! You have a message from Acme Corp.')
    .continue()
    .build();
}
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.

FieldWhat it is
menuResult.menuIdID of the menu that completed (you set this in runMenu).
menuResult.type'return', 'sequence', 'timeout', 'hangup', 'invalidinput'
menuResult.valueThe option's action value, or the entered digit sequence.
menuResult.inputMethod'dtmf' or 'voice'.

Result types

TypeMeaning
returnCaller selected an option. value is the option's action value.
sequenceCaller entered multi-digit input. value is the digit string.
timeoutCaller didn't respond in time.
hangupCaller hung up during the menu.
invalidinputCaller 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.

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

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

  switch (result?.value) {
    case 'sales':
      return new PieSvamlBuilder().say('Connecting you to sales.').connectPstn('+15551111111').build();
    case 'support':
      return new PieSvamlBuilder().say('Connecting you to support.').connectPstn('+15552222222').build();
    default:
      return new PieSvamlBuilder().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

FieldWhat it is
callidCall ID.
durationCall duration in seconds.
reasonWhy the call ended — see below.
resultCall result — see below.
fromOriginating number.
to.endpointDestination.

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

async dice(context, data) {
  const cli = await context.cache.get<string>(`call:${data.callid}:cli`);
  context.log?.info(`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.

// Node.js
import type { VoiceFunction } from '@sinch/functions-runtime';
import { IceSvamlBuilder, PieSvamlBuilder, AceSvamlBuilder, MenuTemplates } from '@sinch/functions-runtime';

export default {
  async ice(context, data) {
    await context.cache.set(`call:${data.callid}:cli`, data.cli ?? 'unknown', 3600);
    return new IceSvamlBuilder()
      .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 new PieSvamlBuilder().say('Goodbye!').hangup().build();
    }
    const dest = { sales: '+15551111111', support: '+15552222222' }[value ?? ''];
    return dest
      ? new PieSvamlBuilder().say(`Connecting you to ${value}.`).connectPstn(dest).build()
      : new PieSvamlBuilder().say('Invalid selection.').continue().build();
  },

  async ace(context, data) {
    return data.amd?.status === 'machine'
      ? new AceSvamlBuilder().hangup().build()
      : new AceSvamlBuilder().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:

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