# Build Call Routing

Route inbound calls based on business hours, caller ID, or geography. All routing decisions happen in your `incoming` (or `manage`) handler and produce a `dial` command.

## Create from template

```bash
sinch functions init simple-voice-ivr --name my-router
cd my-router
sinch functions dev
```

## Time-based routing

Node.js
```typescript
import { onCall } from '@sinch/functions-runtime/voice';
import { createUniversalConfig } from '@sinch/functions-runtime';

function isBusinessHours(timezone = 'America/New_York'): boolean {
  const now = new Date();
  const local = new Date(now.toLocaleString('en-US', { timeZone: timezone }));
  const day = local.getDay();
  const hour = local.getHours();
  return day >= 1 && day <= 5 && hour >= 9 && hour < 17;
}

export const voiceWebhook = onCall({
  incoming: (call, builder, context) => {
    const config = createUniversalConfig(context);
    const officeNumber = config.requireVariable('OFFICE_NUMBER');
    const voicemailNumber = config.requireVariable('VOICEMAIL_NUMBER');

    if (isBusinessHours()) {
      return builder.answer().say('Connecting you to our team.').dialPhone(officeNumber);
    }

    return builder
      .answer()
      .say(
        'Our office is currently closed. We are open Monday through Friday, 9 AM to 5 PM Eastern.'
      )
      .dialPhone(voicemailNumber);
  },
});
```

C#
```csharp
using SinchFunctions.Voice.V2;

namespace SinchFunctions
{
    public class FunctionController(FunctionContext context, IConfiguration configuration, ILogger<FunctionController> logger)
        : SinchVoiceController(context, configuration, logger)
    {
        protected override CallFlow OnIncoming(Call call, CommandBuilder builder)
        {
            var officeNumber = Configuration["OFFICE_NUMBER"]
                ?? throw new InvalidOperationException("OFFICE_NUMBER is required");
            var voicemailNumber = Configuration["VOICEMAIL_NUMBER"]
                ?? throw new InvalidOperationException("VOICEMAIL_NUMBER is required");

            if (IsBusinessHours())
            {
                return builder.Answer().Say("Connecting you to our team.").DialPhone(officeNumber);
            }

            return builder
                .Answer()
                .Say("Our office is currently closed. We are open Monday through Friday, 9 AM to 5 PM Eastern.")
                .DialPhone(voicemailNumber);
        }

        private static bool IsBusinessHours(string timezone = "Eastern Standard Time")
        {
            var tz = TimeZoneInfo.FindSystemTimeZoneById(timezone);
            var local = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, tz);
            return local.DayOfWeek >= DayOfWeek.Monday
                && local.DayOfWeek <= DayOfWeek.Friday
                && local.Hour >= 9 && local.Hour < 17;
        }
    }
}
```

## Caller ID-based routing

Node.js
```typescript
import { onCall, endpointNumber } from '@sinch/functions-runtime/voice';

const VIP_LIST = new Set(['+15550001111', '+15550002222']);
const BLOCKED_LIST = new Set(['+19990001111']);

export const voiceWebhook = onCall({
  incoming: (call, builder) => {
    const caller = endpointNumber(call.from);

    if (BLOCKED_LIST.has(caller)) {
      return builder.answer().say('We are unable to take your call. Goodbye.').hangup();
    }

    if (VIP_LIST.has(caller)) {
      return builder
        .answer()
        .say('Welcome. Connecting you to our priority line.')
        .dialPhone('+15551110010', { from: caller });
    }

    return builder
      .answer()
      .say('Thank you for calling. Connecting you now.')
      .dialPhone('+15551110001');
  },
});
```

C#
```csharp
using SinchFunctions.Voice.V2;

private static readonly HashSet<string> VipList = new() { "+15550001111", "+15550002222" };
private static readonly HashSet<string> BlockedList = new() { "+19990001111" };

protected override CallFlow OnIncoming(Call call, CommandBuilder builder)
{
    // call.FromNumber pulls the number out of whichever Endpoint kind From is,
    // or null for anything else (a stream, voice relay, or an absent endpoint).
    var caller = call.FromNumber ?? "";

    if (BlockedList.Contains(caller))
        return builder.Answer().Say("We are unable to take your call. Goodbye.").Hangup();

    if (VipList.Contains(caller))
        return builder
            .Answer().Say("Welcome. Connecting you to our priority line.")
            .DialPhone("+15551110010", new BridgeToOptions { From = caller });

    return builder
        .Answer().Say("Thank you for calling. Connecting you now.")
        .DialPhone("+15551110001");
}
```

## Geographic routing

Node.js
```typescript
function getRegion(number: string): 'us' | 'uk' | 'eu' | 'default' {
  if (number.startsWith('+1')) return 'us';
  if (number.startsWith('+44')) return 'uk';
  if (number.startsWith('+3') || number.startsWith('+4')) return 'eu';
  return 'default';
}

const REGIONAL_NUMBERS: Record<string, string> = {
  us: '+15551110001',
  uk: '+441110000001',
  eu: '+33110000001',
  default: '+15551110001',
};

export const voiceWebhook = onCall({
  incoming: (call, builder) => {
    const region = getRegion(endpointNumber(call.from));
    return builder
      .answer()
      .say('Connecting you to your regional support team.')
      .dialPhone(REGIONAL_NUMBERS[region]);
  },
});
```

C#
```csharp
private static string GetRegion(string number) => number switch
{
    var n when n.StartsWith("+1")  => "us",
    var n when n.StartsWith("+44") => "uk",
    var n when n.StartsWith("+3") || n.StartsWith("+4") => "eu",
    _ => "default"
};

private static readonly Dictionary<string, string> RegionalNumbers = new()
{
    ["us"] = "+15551110001", ["uk"] = "+441110000001",
    ["eu"] = "+33110000001", ["default"] = "+15551110001",
};

protected override CallFlow OnIncoming(Call call, CommandBuilder builder)
{
    var region = GetRegion(call.FromNumber ?? "");
    return builder
        .Answer()
        .Say("Connecting you to your regional support team.")
        .DialPhone(RegionalNumbers[region]);
}
```

## Dial options

`dialPhone`/`dialSip`/`dialStream`/`dialRelay` cover the common case — bridge a leg onto the live
call, tear it down when either side hangs up. Reach for the lower-level `dial` (below) only when
you want to hand-build that bridge yourself, or dial a leg that never joins one — for example
ringing two destinations in parallel.

Node.js
```typescript
return commands()
  .answer()
  .dial('+15551234567', {
    from: '+15550000000', // Caller ID shown to recipient
    maxDuration: 3600, // Max call length in seconds
    timeout: 30, // Ring timeout, max 60 seconds
    callName: 'destination', // Name this leg for endOnHangup/endOnFailure elsewhere
    endOnHangup: 'origin', // End another named leg when this one hangs up
    endOnFailure: 'origin', // End another named leg if this one never connects
    onAnswer: (c) => c.say('Connected.'), // required once any other event below is set
  })
  .build();
```

C#
```csharp
return new CommandBuilder()
    .Answer()
    .Dial("+15551234567", new DialOptions
    {
        From = "+15550000000",
        MaxDuration = 3600,
        Timeout = 30,
        CallName = "destination",
        EndOnHangup = "origin",
        EndOnFailure = "origin",
    })
    .Build();
```

## SIP routing

Node.js
```typescript
return builder
  .answer()
  .say('Connecting via SIP.')
  .dialSip('sip:support@pbx.example.com', {
    transport: 'TLS',
    headers: { 'X-Custom-Header': 'value' },
  });
```

C#
```csharp
return builder
    .Answer()
    .Say("Connecting via SIP.")
    .DialSip("sip:support@pbx.example.com", new DialSipOptions
    {
        Transport = SipTransport.Tls,
        Headers = new Dictionary<string, string> { ["X-Custom-Header"] = "value" },
    });
```

## Fallback handling

A dial's outcome events (`onBusy`, `onReject`, `onTimeout`, `onFailure`) run inline, in the same response — there is no separate round trip to re-route after a failed leg the way v1's DICE callback needed one.

Node.js
```typescript
export const voiceWebhook = onCall({
  incoming: (call, builder) =>
    builder.answer().dial('+15551110001', {
      timeout: 20,
      onAnswer: (c) => c.say('Connected.'),
      onBusy: (c) => c.say('That line is busy. Please try again later.').hangup(),
      onTimeout: (c) => c.say('No one is available right now.').hangup(),
    }),
});
```

C#
```csharp
protected override CallFlow OnIncoming(Call call, CommandBuilder builder) =>
    builder.Answer().Dial("+15551110001", new DialOptions
    {
        Timeout = 20,
        OnAnswer = new CommandBuilder().Say("Connected."),
        OnBusy = new CommandBuilder().Say("That line is busy. Please try again later.").Hangup(),
        OnTimeout = new CommandBuilder().Say("No one is available right now.").Hangup(),
    });
```

For sinch.json configuration and environment variables, see [Templates](/docs/functions/functions/templates) and [Configuration & Secrets](/docs/functions/functions/concepts/configuration-secrets).