fsm_sam
02/19/2025, 4:35 PMKyle Brunker
02/20/2025, 9:14 PMShubham Bajaj
02/20/2025, 9:26 PMSolution
ts
// 1. First, handle incoming Twilio webhook
app.post('/twilio/voice', async (req, res) => {
// Create a Vapi call with bypass enabled
const vapiCall = await fetch('https://api.vapi.ai/call', {
method: 'POST',
body: JSON.stringify({
phoneCallProviderBypassEnabled: true,
// other call parameters...
})
});
const { callId } = await vapiCall.json();
// Return TwiML to connect Twilio to your websocket
const twiml = new VoiceResponse();
const connect = twiml.connect();
connect.stream({
url: `wss://your-server.com/twilio-bridge/${callId}`
});
res.type('text/xml');
res.send(twiml.toString());
});
// 2. Setup WebSocket bridge
const setupCallBridge = async (callId: string, twilioWs: WebSocket) => {
// Important: First establish transport connection
const vapiTransport = new WebSocket(`wss://api.vapi.ai/call/${callId}/transport`);
await new Promise(resolve => vapiTransport.once('open', resolve));
// Only after transport is established, connect to listen endpoint
const vapiListen = new WebSocket(`wss://api.vapi.ai/call/${callId}/listen`);
// Handle Twilio -> Vapi audio
twilioWs.on('message', async (data) => {
try {
const message = JSON.parse(data.toString());
if (message.event === 'media') {
const audio = Buffer.from(message.media.payload, 'base64');
if (vapiTransport.readyState === WebSocket.OPEN) {
vapiTransport.send(audio);
}
}
} catch (error) {
console.error('Error processing Twilio message:', error);
}
});
// Handle Vapi -> Twilio audio
vapiListen.on('message', (data, isBinary) => {
if (!isBinary) return; // Skip non-audio messages
try {
if (twilioWs.readyState === WebSocket.OPEN) {
twilioWs.send(JSON.stringify({
event: 'media',
streamSid: callId,
media: {
payload: data.toString('base64'),
track: 'outbound'
}
}));
}
} catch (error) {
console.error('Error sending audio to Twilio:', error);
}
});
// Error handling
const handleError = (ws: WebSocket, source: string) => {
ws.on('error', (error) => {
console.error(`${source} WebSocket error:`, error);
});
ws.on('close', (code, reason) => {
console.log(`${source} WebSocket closed:`, code, reason.toString());
});
};
handleError(vapiTransport, 'Vapi Transport');
handleError(vapiListen, 'Vapi Listen');
handleError(twilioWs, 'Twilio');
};
Key Points:
- First establish transport then connect listen. This addresses the 80% failure rate because the /listen endpoint expects the transport connection to be established first.
- Using separate connections for input/output helps maintain clean audio streams and prevents potential conflicts.
- Proper Audio Format Handling:
ts
// Twilio sends base64, convert to buffer for Vapi
const audio = Buffer.from(message.media.payload, 'base64');
// Vapi sends binary, convert to base64 for Twilio
payload: data.toString('base64')