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.
sinch functions init call-forwarding --name my-router
cd my-router
sinch functions devimport 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;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;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 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;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();return createIceBuilder()
.say('Connecting via SIP.')
.connectSip('sip:support@pbx.example.com', {
cli: event.cli,
headers: { 'X-Custom-Header': 'value' },
})
.build();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;For sinch.json configuration and environment variables, see Templates and Configuration & Secrets.