Skip to content

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

sinch functions init call-forwarding --name my-router
cd my-router
sinch functions dev

Time-based routing

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;

Caller ID-based routing

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;

Geographic routing

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;

ConnectPstn options

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

SIP routing

return createIceBuilder()
  .say('Connecting via SIP.')
  .connectSip('sip:support@pbx.example.com', {
    cli: event.cli,
    headers: { 'X-Custom-Header': 'value' },
  })
  .build();

Fallback handling

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.