Appearance
Phone Number Routing: Multiple Assistants, One Webhook
When you have multiple phone numbers - each for a different purpose - you don't need separate webhook endpoints. Route them all to a single endpoint and dispatch based on the called number.
The Pattern
Every sipgate AI Flow event includes the phone number in the session object:
json
{
"type": "session_start",
"session": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"to_phone_number": "+4921234567890",
"from_phone_number": "+4915112345678",
"direction": "inbound"
}
}Use to_phone_number to determine which assistant or behavior to use.
Basic Routing
typescript
const ASSISTANTS = {
'+4921234567890': {
name: 'Sales',
greeting: 'Hi! Thanks for calling our sales team. How can I help?',
systemPrompt: 'You are a helpful sales assistant...',
},
'+4921234567891': {
name: 'Support',
greeting: 'Hello! You\'ve reached customer support. What can I help you with?',
systemPrompt: 'You are a friendly support agent...',
},
'+4921234567892': {
name: 'Booking',
greeting: 'Welcome! I can help you book an appointment. When would you like to come in?',
systemPrompt: 'You are an appointment booking assistant...',
},
}
export async function POST(req: Request) {
const event = await req.json()
const phoneNumber = event.session.to_phone_number
// Get assistant config for this number
const assistant = ASSISTANTS[phoneNumber]
if (!assistant) {
// Unknown number - use fallback
return speak(event.session.id, "Sorry, this number is not configured.")
}
switch (event.type) {
case 'session_start':
return speak(event.session.id, assistant.greeting)
case 'user_speak':
return handleUserSpeak(event, assistant)
// ... other events
}
}Database-Driven Routing
For dynamic configuration, store the mapping in a database:
typescript
// Database schema
// phone_numbers: id, phone_number, assistant_id
// assistants: id, name, greeting, system_prompt, voice_provider, voice_id
async function getAssistantForNumber(phoneNumber: string) {
const { data } = await supabase
.from('phone_numbers')
.select(`
phone_number,
assistants (
id,
name,
greeting,
system_prompt,
voice_provider,
voice_id
)
`)
.eq('phone_number', phoneNumber)
.single()
return data?.assistants
}
export async function POST(req: Request) {
const event = await req.json()
const phoneNumber = event.session.to_phone_number
const assistant = await getAssistantForNumber(phoneNumber)
if (!assistant) {
return speak(event.session.id, "This number is not currently in service.")
}
// Route to appropriate handler
return handleEvent(event, assistant)
}Normalizing Phone Numbers
Phone numbers can arrive in different formats. Normalize before lookup:
typescript
function normalizePhoneNumber(phone: string): string {
// Remove spaces, dashes, parentheses
let normalized = phone.replace(/[\s\-\(\)]/g, '')
// Ensure + prefix
if (!normalized.startsWith('+')) {
normalized = '+' + normalized
}
return normalized
}
async function getAssistantForNumber(phoneNumber: string) {
const normalized = normalizePhoneNumber(phoneNumber)
const { data } = await supabase
.from('phone_numbers')
.select('*, assistants(*)')
.eq('phone_number', normalized)
.single()
return data?.assistants
}Multi-Language Routing
Route to different languages based on phone number:
typescript
const LANGUAGE_NUMBERS = {
'+4921234567890': { language: 'de-DE', voice: 'de-DE-KatjaNeural' },
'+4421234567890': { language: 'en-GB', voice: 'en-GB-SoniaNeural' },
'+3321234567890': { language: 'fr-FR', voice: 'fr-FR-DeniseNeural' },
}
function getLanguageConfig(phoneNumber: string) {
return LANGUAGE_NUMBERS[phoneNumber] || {
language: 'en-US',
voice: 'en-US-JennyNeural',
}
}
export async function POST(req: Request) {
const event = await req.json()
const langConfig = getLanguageConfig(event.session.to_phone_number)
// Use language-specific TTS
return Response.json({
type: 'speak',
session_id: event.session.id,
text: getGreeting(langConfig.language),
tts: {
provider: 'azure',
language: langConfig.language,
voice: langConfig.voice,
},
})
}Routing by Caller Number
You can also route based on who's calling (from_phone_number):
typescript
async function handleSessionStart(event: SessionStartEvent) {
const callerNumber = event.session.from_phone_number
// Check if this is a known VIP customer
const customer = await getCustomerByPhone(callerNumber)
if (customer?.is_vip) {
return speak("Welcome back! I see you're a VIP member. How can I assist you today?")
}
// Check if this is a repeat caller
const previousCalls = await getRecentCalls(callerNumber)
if (previousCalls.length > 0) {
const lastTopic = previousCalls[0].topic
return speak(`Hello again! Are you calling about ${lastTopic}, or something new?`)
}
// First-time caller
return speak("Welcome! How can I help you today?")
}Fallback Handling
Always handle unknown numbers gracefully:
typescript
async function getAssistantForNumber(phoneNumber: string) {
const normalized = normalizePhoneNumber(phoneNumber)
const { data } = await supabase
.from('phone_numbers')
.select('*, assistants(*)')
.eq('phone_number', normalized)
.single()
if (!data?.assistants) {
// Log for debugging
console.warn(`No assistant configured for: ${normalized}`)
// Return a default fallback assistant
return {
id: 'fallback',
name: 'Fallback',
greeting: "I'm sorry, but this number is not currently configured. Please try again later.",
system_prompt: 'Politely explain that the service is unavailable.',
voice_provider: 'azure',
voice_id: 'en-US-JennyNeural',
}
}
return data.assistants
}Caching for Performance
If you're looking up the same numbers repeatedly, cache the results:
typescript
const assistantCache = new Map<string, { assistant: Assistant; cachedAt: number }>()
const CACHE_TTL_MS = 60000 // 1 minute
async function getAssistantForNumber(phoneNumber: string): Promise<Assistant> {
const normalized = normalizePhoneNumber(phoneNumber)
// Check cache
const cached = assistantCache.get(normalized)
if (cached && Date.now() - cached.cachedAt < CACHE_TTL_MS) {
return cached.assistant
}
// Fetch from database
const { data } = await supabase
.from('phone_numbers')
.select('*, assistants(*)')
.eq('phone_number', normalized)
.single()
const assistant = data?.assistants || getFallbackAssistant()
// Cache result
assistantCache.set(normalized, { assistant, cachedAt: Date.now() })
return assistant
}Complete Example
typescript
// types.ts
interface Assistant {
id: string
name: string
greeting: string
system_prompt: string
voice_provider: 'azure' | 'eleven_labs'
voice_id: string
language: string
}
// routing.ts
const assistantCache = new Map<string, Assistant>()
function normalizePhoneNumber(phone: string): string {
let normalized = phone.replace(/[\s\-\(\)]/g, '')
if (!normalized.startsWith('+')) normalized = '+' + normalized
return normalized
}
async function getAssistant(phoneNumber: string): Promise<Assistant> {
const normalized = normalizePhoneNumber(phoneNumber)
// Check cache
if (assistantCache.has(normalized)) {
return assistantCache.get(normalized)!
}
// Fetch from database
const { data } = await db
.from('phone_numbers')
.select('*, assistants(*)')
.eq('phone_number', normalized)
.single()
const assistant = data?.assistants || {
id: 'fallback',
name: 'Fallback',
greeting: 'This number is not configured.',
system_prompt: 'Explain the service is unavailable.',
voice_provider: 'azure',
voice_id: 'en-US-JennyNeural',
language: 'en-US',
}
assistantCache.set(normalized, assistant)
return assistant
}
// webhook.ts
export async function POST(req: Request): Promise<Response> {
const event = await req.json()
const sessionId = event.session.id
// Route to assistant based on called number
const assistant = await getAssistant(event.session.to_phone_number)
switch (event.type) {
case 'session_start':
console.log(`Call to ${assistant.name} assistant`)
return speak(sessionId, assistant.greeting, assistant)
case 'user_speak':
const response = await generateLLMResponse(event.text, assistant)
return speak(sessionId, response, assistant)
case 'session_end':
return new Response(null, { status: 204 })
default:
return new Response(null, { status: 204 })
}
}
function speak(sessionId: string, text: string, assistant: Assistant): Response {
const ttsConfig = assistant.voice_provider === 'azure'
? {
provider: 'azure' as const,
language: assistant.language,
voice: assistant.voice_id,
}
: {
provider: 'eleven_labs' as const,
voice: assistant.voice_id,
}
return Response.json({
type: 'speak',
session_id: sessionId,
text,
tts: ttsConfig,
})
}Best Practices
Normalize phone numbers - Handle different formats (+49, 0049, 049, etc.)
Always have a fallback - Unknown numbers should get a polite message, not an error
Cache lookups - Database queries on every event add latency
Log unknown numbers - Helps you spot misconfiguration
Use
to_phone_number- That's the number they dialed (your number)Consider
from_phone_number- For personalization based on caller
Related Documentation
- Session Start Event - Event structure with phone numbers
- HTTP Webhooks - Webhook endpoint setup