Claude Tools

Real-Time Voice Agents: Claude API Integration with Twilio and WebSockets

Build ultra-low-latency voice agents with Claude API, Twilio for calls, and WebSockets for streaming audio. Experience natural, interruption-friendly conversations powered by Claude 3.5 Sonnet.

A

Andrew Snyder

AI & Automation Editor

December 19, 2025 min read
Share:

Why Real-Time Voice Agents with Claude?

Hey there, Claude enthusiasts! Imagine picking up the phone and chatting with an AI that feels eerily human—quick responses, handles interruptions gracefully, and powered by the brilliant reasoning of Claude 3.5 Sonnet. That's the magic of real-time voice agents.

Traditional voice bots? Clunky. They wait for you to finish speaking, process in batch, and spit out a response after an awkward pause. With streaming WebSockets, STT, Claude's live streaming API, and TTS, we cut latency to under 500ms end-to-end. Perfect for customer support, virtual assistants, or even role-playing games.

In this guide, we'll build one from scratch using Twilio for telephony, Deepgram for speech-to-text, Anthropic Claude API for smarts, and ElevenLabs for voice synthesis. All streamed via WebSockets for real-time magic.

We'll compare batch vs. streaming approaches along the way, with full Node.js code you can copy-paste.

Architecture: How It All Fits Together

Here's the flow:

  1. Caller dials Twilio number → Twilio connects call and starts Media Stream via WebSocket to your server.
  2. Your server receives raw audio packets → Streams to Deepgram for real-time STT → Partial transcripts arrive every 200ms.
  3. Transcripts fed to Claude incrementally → Claude streams tokens back via its /v1/messages streaming endpoint.
  4. Claude response → ElevenLabs TTS → Audio chunks streamed back through Twilio WebSocket to caller.
  5. Interruptions? Detect user barge-in via audio energy and flush Claude context.

Architecture Diagram (Pro tip: Use Draw.io for your own diagrams)

Batch vs. Streaming Comparison:

AspectBatch ProcessingStreaming (Our Setup)
Latency2-5s per turn<1s end-to-end
NaturalnessRobotic pausesHuman-like flow
CostLower computeHigher (but Claude's cheap)
ComplexityEasyMedium (worth it!)

Claude shines here: Its 200k token context holds full convo history, and streaming feels snappier than GPT-4o thanks to optimized tokenization.

Prerequisites

Install deps:

npm init -y
npm i express ws @anthropic-ai/sdk deepgram-sdk elevenlabs-node twilio

Step 1: Twilio Setup

  1. Buy a phone number in Twilio Console (Voice > Phone Numbers).
  2. Create a TwiML Bin or webhook with this XML:
<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Start>
    <Stream url="wss://your-ngrok-url.com/audio" />
  </Start>
  <Say>Hi! Chat with Claude now.</Say>
</Response>

Configure your number's webhook to point to a /voice endpoint returning this TwiML.

Pro Tip: Use Twilio's Programmable Voice webhook for dynamic TwiML.

Step 2: WebSocket Server for Audio Streams

Fire up an Express server handling Twilio Media Streams.

// server.js
const express = require('express');
const WebSocket = require('ws');
const { Anthropic } = require('@anthropic-ai/sdk');
const { createClient } = require('@deepgram/sdk');
const ElevenLabs = require('elevenlabs-node');

const app = express();
const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
const deepgram = createClient(process.env.DEEPGRAM_KEY);
const elevenlabs = new ElevenLabs({ apiKey: process.env.ELEVENLABS_KEY });

app.use(express.static('public'));

// WebSocket server for Twilio

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws, req) => {
  const callSid = req.url.split('/')[2]; // Extract from Twilio path
  let conversation = [{ role: 'user', content: 'You are a helpful assistant.' }];
  let buffer = '';

  ws.on('message', async (data) => {
    const message = JSON.parse(data);

    if (message.event === 'media') {
      // Stream audio to Deepgram
      const transcript = await deepgram.transcription.live({
        punctuate: true,
        interim_results: true,
      }, {
        model: 'nova-2',
        language: 'en',
        smart_format: true,
      }).send(message.media.payload);

      if (transcript && transcript.channel.alternatives[0].transcript) {
        buffer += transcript.channel.alternatives[0].transcript;

        if (transcript.channel.alternatives[0].is_final) {
          // Final transcript: send to Claude
          conversation.push({ role: 'user', content: buffer });
          buffer = '';

          const stream = await anthropic.messages.create({
            model: 'claude-3-5-sonnet-20241022',
            max_tokens: 1024,
            messages: conversation,
            stream: true,
          });

          let responseText = '';
          for await (const chunk of stream) {
            if (chunk.type === 'content_block_delta') {
              responseText += chunk.delta.text;
              // Stream partial to TTS
              const audioChunk = await elevenlabs.textToSpeech({
                text: responseText.slice(-50), // Last chunk
                voice: 'Rachel', // Natural voice
                model: 'eleven_monolingual_v1',
                stream: true,
              });

              // Send audio back via Twilio WS
              ws.send(JSON.stringify({
                event: 'media',
                streamSid: message.streamSid,
                media: { payload: audioChunk.toString('base64') },
              }));
            }
          }

          conversation.push({ role: 'assistant', content: responseText });
        }
      }
    }
  });

  ws.on('close', () => {
    console.log('Call ended');
  });
});

app.post('/voice', (req, res) => {
  // Return TwiML here
  res.type('text/xml');
  res.send(`<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Start><Stream url="wss://${req.headers.host}/ws/${callSid}" /></Start>
  <Say>Welcome to Claude Voice Agent!</Say>
</Response>`);
});

const server = app.listen(3000, () => console.log('Server on 3000'));

Key Notes:

  • This is simplified—add VAD (voice activity detection) for barge-in using message.media.payload energy levels.
  • Deepgram's Live Transcription SDK handles real-time STT perfectly (~300ms latency).
  • Claude streaming via Anthropic SDK: chunks arrive every ~200ms.

Step 3: Enhancing with Interruptions & Context

Claude's long context is gold, but for voice:

  • Flush on barge-in: If new audio > threshold during Claude response, send /abort to stream and restart.
  • System prompt: Tune for domain, e.g., "You are a sales agent for SaaS tools."

Updated code snippet for interruption:

// In message handler
if (message.event === 'media' && isSpeaking) { // VAD check
  // Interrupt Claude stream
  claudeStream.controller.abort();
  isSpeaking = true;
}

Step 4: TTS Streaming with ElevenLabs

ElevenLabs supports chunked streaming—feed partial Claude text for progressive audio.

Latency Comparison:

ProviderStreaming LatencyVoice QualityClaude Integration
ElevenLabs250ms🎤 ExcellentNative Node SDK
PlayHT400msGoodAPI only
Google TTS800msNaturalHigher cost

Deployment & Scaling

  1. Deploy to Railway, Vercel, or Fly.io.
  2. Use ngrok: ngrok http 3000 → Update Twilio webhook.
  3. Scale: Redis for multi-call context, PM2 for clustering.

Costs (per hour call):

  • Twilio: $0.0085/min
  • Deepgram: $0.0043/min
  • Claude: ~$0.003 (10k tokens)
  • ElevenLabs: $0.18/1k chars Total: <$0.05/min

Testing Your Agent

  1. Call your Twilio #.
  2. Say: "What's the best way to prompt Claude for code?"
  3. Marvel at the fluid response!

Debug: Twilio Inspector for WS traces, Deepgram console for transcripts.

Limitations & Comparisons

  • Claude vs. GPT-4o: Claude cheaper ($3/1M input vs. $5), better reasoning, but GPT has native voice mode (Realtime API). Claude wins on cost/complexity for custom stacks.
  • Edge cases: Heavy accents? Deepgram Nova-2 handles 30+ langs. Noisy? Add noise suppression.
  • Enterprise: Add auth, logging via MCP or custom agents.

Next Steps

  • Add RAG: Pinecone vector DB for knowledge base.
  • Multi-turn tools: Call Claude Functions mid-convo.
  • Integrate with n8n for workflows.

Fork this on GitHub, tweak, and share your builds in comments! Questions? Drop 'em below.

Word count: ~1450

The #1 Newsletter in AI

Stay ahead of the AI curve

The most important updates, news, and content — delivered in one weekly newsletter.

No spam. Unsubscribe anytime. Privacy policy

Claude API
Voice Agents
Twilio
WebSockets
Real-time Streaming
ai-agents
A

About Andrew Snyder

AI & Automation Editor

Andrew covers practical AI automation, workflow design, and the tools teams use to streamline everyday operations.

Comments (0)