Connect inbound calls to an AI voice agent powered by ElevenLabs Conversational AI. The runtime handles SIP bridging; you write the webhook logic that personalizes each conversation.
- Caller dials your Sinch number.
- Sinch sends an ICE callback to your function.
- Your function responds with a
connectAgentaction targeting ElevenLabs via SIP. - Sinch bridges the call to ElevenLabs.
- (Optional) ElevenLabs sends a conversation-init webhook to personalize the greeting.
- After the call ends, ElevenLabs sends a post-call webhook with transcript and analysis.
ELEVENLABS_AGENT_ID=your-agent-id
ELEVENLABS_API_KEY=your-api-key # for outbound calls and post-call webhooksimport type { VoiceFunction, FunctionContext } from '@sinch/functions-runtime';
import type { Voice } from '@sinch/voice';
import { createIceBuilder, AgentProvider, createUniversalConfig } from '@sinch/functions-runtime';
export default {
async ice(context: FunctionContext, event: Voice.IceRequest) {
const config = createUniversalConfig(context);
const agentId = config.requireSecret('ELEVENLABS_AGENT_ID');
return createIceBuilder()
.say('Connecting you to our AI assistant. Please hold.')
.connectAgent(AgentProvider.ElevenLabs, agentId)
.build();
},
} satisfies VoiceFunction;return createIceBuilder()
.connectAgent(AgentProvider.ElevenLabs, agentId, {
cli: event.cli, // Pass caller ID to agent SIP trunk
maxDuration: 1800, // 30-minute max
suppressCallbacks: false, // Still receive ACE/DICE callbacks
})
.build();Show a menu first and connect to the agent only if the caller opts in:
import {
createIceBuilder,
createPieBuilder,
MenuBuilder,
AgentProvider,
createUniversalConfig,
} from '@sinch/functions-runtime';
export default {
async ice(context: FunctionContext, event: Voice.IceRequest) {
const menu = new MenuBuilder()
.prompt('Press 1 to speak with our AI assistant, or press 0 to reach a human agent.')
.option('1', 'return(ai_agent)')
.option('0', 'return(human_agent)')
.timeout(5000)
.repeats(2)
.build();
return createIceBuilder().runMenu(menu).build();
},
async pie(context: FunctionContext, event: Voice.PieRequest) {
const config = createUniversalConfig(context);
const agentId = config.requireSecret('ELEVENLABS_AGENT_ID');
const humanNumber = config.requireVariable('HUMAN_AGENT_NUMBER');
if (event.menuResult?.value === 'ai_agent') {
return createPieBuilder()
.say('Connecting you to our AI assistant.')
.connectAgent(AgentProvider.ElevenLabs, agentId)
.build();
}
if (event.menuResult?.value === 'human_agent') {
return createPieBuilder()
.say('Connecting you to a human agent.')
.connectPstn(humanNumber)
.build();
}
return createPieBuilder().say('Invalid selection. Goodbye.').hangup().build();
},
} satisfies VoiceFunction;ElevenLabs calls this webhook before the agent's first word. Return dynamic_variables to personalize the session.
Configure the URL in your ElevenLabs agent settings: https://func-{project}-{name}.functions.sinch.com/webhook/elevenlabs/conversation-init
import {
ElevenLabsController,
type ConversationInitRequest,
type ConversationInitResponse,
} from '@sinch/functions-runtime';
class MyElevenLabsHandler extends ElevenLabsController {
async handleConversationInit(
request: ConversationInitRequest
): Promise<ConversationInitResponse> {
const callerId = request.caller_id ?? 'unknown';
return {
dynamic_variables: {
caller_phone: callerId,
customer_name: 'valued customer',
account_tier: 'standard',
},
};
}
async handlePostCall(webhook: ElevenLabsPostCallWebhook): Promise<void> {
const { conversation_id, status, metadata, analysis } = webhook.data;
console.log('Call completed', {
conversationId: conversation_id,
duration: metadata.call_duration_secs,
summary: analysis?.transcript_summary,
});
}
}| Field | Type | Description |
|---|---|---|
dynamic_variables | Record<string, string> | Key-value pairs injected into agent prompt and first message |
conversation_config_override.agent.first_message | string | Override opening line |
conversation_config_override.agent.prompt | string | Override system prompt |
conversation_config_override.agent.language | string | Override language (e.g. "es") |
ElevenLabs initiates the outbound call. Your function bridges the SIP leg to PSTN:
export default {
async ice(context: FunctionContext, event: Voice.IceRequest) {
if (event.originationType === 'SIP') {
const destination = event.to?.endpoint ?? '';
return createIceBuilder().connectPstn(destination, { cli: event.cli }).build();
}
return createIceBuilder().hangup().build();
},
async ace(context: FunctionContext, event: Voice.AceRequest) {
return new AceSvamlBuilder().continue().build();
},
} satisfies VoiceFunction;For sinch.json configuration and environment variables, see Templates and Configuration & Secrets.