# Build Call Routing Route inbound calls based on business hours, caller ID, geography, or custom logic. All routing decisions happen in your ICE or PIE handler and produce a `connectPstn` or `connectSip` action. ## Create from template ```bash sinch functions init call-forwarding --name my-router cd my-router sinch functions dev ``` ## Time-based routing Node.js ```typescript import type { VoiceFunction, FunctionContext } from '@sinch/functions-runtime'; import type { Voice } from '@sinch/voice'; import { createIceBuilder, 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 default { async ice(context: FunctionContext, event: Voice.IceRequest) { const config = createUniversalConfig(context); const officeNumber = config.requireVariable('OFFICE_NUMBER'); const voicemailNumber = config.requireVariable('VOICEMAIL_NUMBER'); if (isBusinessHours()) { return createIceBuilder() .say('Connecting you to our team.') .connectPstn(officeNumber) .build(); } else { return createIceBuilder() .say( 'Our office is currently closed. We are open Monday through Friday, 9 AM to 5 PM Eastern.' ) .connectPstn(voicemailNumber) .build(); } }, } satisfies VoiceFunction; ``` C# ```csharp using Microsoft.AspNetCore.Mvc; using SinchFunctions.Models; using SinchFunctions.Utils; namespace SinchFunctions { public class FunctionController : SinchVoiceController { public FunctionController( FunctionContext context, IConfiguration configuration, ILogger logger) : base(context, configuration, logger) { } public override async Task Ice([FromBody] IceCallbackModel callbackData) { 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 Ok(new IceSvamletBuilder() .Instructions.Say("Connecting you to our team.") .Action.ConnectPstn(officeNumber) .Build()); } return Ok(new IceSvamletBuilder() .Instructions.Say("Our office is currently closed. We are open Monday through Friday, 9 AM to 5 PM Eastern.") .Action.ConnectPstn(voicemailNumber) .Build()); } 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 const VIP_LIST = new Set(['+15550001111', '+15550002222']); const BLOCKED_LIST = new Set(['+19990001111']); export default { async ice(context: FunctionContext, event: Voice.IceRequest) { const caller = event.cli ?? ''; if (BLOCKED_LIST.has(caller)) { return createIceBuilder().say('We are unable to take your call. Goodbye.').hangup().build(); } if (VIP_LIST.has(caller)) { return createIceBuilder() .say('Welcome. Connecting you to our priority line.') .connectPstn('+15551110010', { cli: caller }) .build(); } return createIceBuilder() .say('Thank you for calling. Connecting you now.') .connectPstn('+15551110001') .build(); }, } satisfies VoiceFunction; ``` C# ```csharp private static readonly HashSet VipList = new() { "+15550001111", "+15550002222" }; private static readonly HashSet BlockedList = new() { "+19990001111" }; public override async Task Ice([FromBody] IceCallbackModel callbackData) { var caller = callbackData.Cli ?? ""; if (BlockedList.Contains(caller)) return Ok(new IceSvamletBuilder() .Instructions.Say("We are unable to take your call. Goodbye.") .Action.Hangup().Build()); if (VipList.Contains(caller)) return Ok(new IceSvamletBuilder() .Instructions.Say("Welcome. Connecting you to our priority line.") .Action.ConnectPstn("+15551110010", cli: caller).Build()); return Ok(new IceSvamletBuilder() .Instructions.Say("Thank you for calling. Connecting you now.") .Action.ConnectPstn("+15551110001").Build()); } ``` ## 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 = { us: '+15551110001', uk: '+441110000001', eu: '+33110000001', default: '+15551110001', }; export default { async ice(context: FunctionContext, event: Voice.IceRequest) { const region = getRegion(event.cli ?? ''); return createIceBuilder() .say('Connecting you to your regional support team.') .connectPstn(REGIONAL_NUMBERS[region]) .build(); }, } satisfies VoiceFunction; ``` 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 RegionalNumbers = new() { ["us"] = "+15551110001", ["uk"] = "+441110000001", ["eu"] = "+33110000001", ["default"] = "+15551110001", }; public override async Task Ice([FromBody] IceCallbackModel callbackData) { var region = GetRegion(callbackData.Cli ?? ""); return Ok(new IceSvamletBuilder() .Instructions.Say("Connecting you to your regional support team.") .Action.ConnectPstn(RegionalNumbers[region]).Build()); } ``` ## ConnectPstn options Node.js ```typescript return createIceBuilder() .connectPstn('+15551234567', { cli: '+15550000000', // Caller ID shown to recipient maxDuration: 3600, // Max call length in seconds timeout: 30, // Ring timeout enableAce: true, // Receive ACE callback when answered enablePie: true, // Receive PIE callback for DTMF enableDice: true, // Receive DICE callback on disconnect indications: 'us', // Ring tone country }) .build(); ``` C# ```csharp return Ok(new IceSvamletBuilder() .Action.ConnectPstn("+15551234567", new ConnectPstnOptions { Cli = "+15550000000", MaxDuration = 3600, ConnectTimeout = 30, Indications = "us", }) .Build()); ``` ## SIP routing Node.js ```typescript return createIceBuilder() .say('Connecting via SIP.') .connectSip('sip:support@pbx.example.com', { cli: event.cli, headers: { 'X-Custom-Header': 'value' }, }) .build(); ``` C# ```csharp return Ok(new IceSvamletBuilder() .Instructions.Say("Connecting via SIP.") .Action.ConnectSip("sip:support@pbx.example.com", new ConnectSipOptions { Cli = callbackData.Cli, Headers = new Dictionary { ["X-Custom-Header"] = "value" } }) .Build()); ``` ## Fallback handling Node.js ```typescript export default { async ice(context: FunctionContext, event: Voice.IceRequest) { return createIceBuilder() .connectPstn('+15551110001', { enableAce: true, enableDice: true, timeout: 20 }) .build(); }, async dice(context: FunctionContext, event: Voice.DiceRequest) { if (event.reason === 'noAnswer' || event.reason === 'busy') { // DICE fires after disconnect — re-routing is not possible here. // Use an IVR menu or park action for active fallback. console.warn('Primary destination did not answer:', event.reason); } }, } satisfies VoiceFunction; ``` C# ```csharp public override async Task Ice([FromBody] IceCallbackModel callbackData) { return Ok(new IceSvamletBuilder() .Action.ConnectPstn("+15551110001", new ConnectPstnOptions { ConnectTimeout = 20 }) .Build()); } public override async Task Dice([FromBody] DiceCallbackModel callbackData) { if (callbackData.Reason == "noAnswer" || callbackData.Reason == "busy") Logger.LogWarning("Primary destination did not answer: {Reason}", callbackData.Reason); return Ok(); } ``` For sinch.json configuration and environment variables, see [Templates](#) and [Configuration & Secrets](#).