Build a Voice Agent using Vobiz
Overview
This guide shows you how to build a real-time voice agent that answers phone calls, using Vobiz for telephony and Sarvam AI for everything else: speech-to-text, the LLM, and text-to-speech. Unlike the other integration guides in this section, this one doesn’t use Pipecat. Vobiz’s bidirectional media stream is a plain JSON-over-WebSocket protocol, so a small FastAPI server talking to Sarvam’s REST API directly is all you need.
What You’ll Build
A voice agent that can:
- Answer inbound phone calls on a Vobiz number
- Listen to callers speaking, in multiple Indian languages
- Understand and process what they say, with barge-in support
- Respond back in a natural-sounding voice, over the phone, sentence by sentence as the reply streams in
Quick Overview
- Get an API key (Sarvam). Vobiz needs no API credentials for this flow.
- Install packages:
pip install fastapi httpx python-dotenv python-multipart "uvicorn[standard]" - Create a
.envfile with your API key and public URL - Write a FastAPI server that speaks Vobiz’s WebSocket media protocol and calls Sarvam directly
- Create a Vobiz application pointing at your server, and attach your number to it
- Call your Vobiz number
Quick Start
1. Prerequisites
- Python 3.9 or higher
- ngrok, to expose your local server during development
- A Vobiz account with a provisioned voice number
- A Sarvam AI API key from your dashboard
Vobiz doesn’t need an account SID, auth token, or any credentials in your agent code for this flow. It fetches your Answer URL as plain HTTP when a call comes in, and everything it needs to route the call, including the stream and call identifiers, arrives over the WebSocket itself, just like Exotel’s Voicebot Applet.
2. Install Dependencies
macOS/Linux
Windows
This guide talks to Sarvam over plain httpx calls rather than the sarvamai SDK or Pipecat, so the whole pipeline has one HTTP dependency and one authentication header (api-subscription-key). That also means audio format handling, including resampling, WAV wrapping, and container stripping, is your responsibility here, unlike the Pipecat-based guides where the transport and Sarvam services handle it for you.
3. Create Environment File
Create a file named .env in your project folder:
Replace SARVAM_API_KEY with your real key from the Sarvam dashboard, and PUBLIC_URL with your ngrok URL once you have one (Step 5).
sarvam-30b has been deprecated. sarvam-105b is now the only supported chat model, and the /v1/chat/completions API rejects the model="sarvam-30b" value. If you have existing agents pinned to sarvam-30b, update them to sarvam-105b before it stops responding entirely.
sarvam-105b reasons before replying, and that reasoning is billed against max_tokens. Budget at least 1500 tokens per turn. Set it too low, and the API returns a valid response with empty content, which would otherwise strand the caller in silence.
4. Write the Sarvam Client
Create sarvam.py, a thin async wrapper over the three Sarvam endpoints the agent needs: speech-to-text, chat, and text-to-speech.
raw_linear16() strips a WAV header if Sarvam returns one. Vobiz’s playAudio message requires raw, container-free Linear16 samples: sending it a WAV file (with its 44-byte header) produces a burst of noise at the start of every utterance.
5. Write the Call Session
Create agent.py. This holds the per-call pipeline: a simple energy-based VAD (voice activity detector) over inbound audio, then speech-to-text, the chat model, and text-to-speech, streamed back to the caller sentence by sentence:
The chat model’s streamed reply is split into sentences with SENTENCE_END and spoken as each one completes, rather than waiting for the full reply. This is what keeps turn latency down: the caller hears sentence one while the model is still generating sentence two.
Barge-in works by watching inbound RMS energy during playback: if the caller starts talking while the agent is still speaking, the session sends clearAudio and starts listening for a new utterance. This energy-based VAD can’t distinguish speech from background noise, so on noisy phone lines you may need to raise SILENCE_THRESHOLD or disable barge-in entirely.
6. Write the FastAPI Server
Create server.py, exposing the Answer URL Vobiz fetches when a call comes in, and the WebSocket it streams audio over:
<Record> and <Stream> are siblings under <Response>, not nested inside one another. If you add call recording later with <Record recordSession="true" .../>, place it as its own top-level element alongside <Stream>. Nesting <Stream> inside <Record>, or letting a Record callback return <Hangup/>, ends the call before the agent WebSocket ever starts.
7. Configure Vobiz to Reach Your Agent
Start ngrok:
ngrok prints a forwarding URL that looks like https://xxxx.ngrok-free.app. Put that exact URL, unchanged, into PUBLIC_URL in your .env file. The server derives the wss:// WebSocket URL from it automatically.
Free ngrok URLs change every time you restart ngrok. If your agent stops receiving calls, update PUBLIC_URL in .env to match the URL ngrok is currently printing, and restart server.py.
Create an application and point it at the server:
The Answer and Hangup URLs live on a Vobiz application, not directly on the number. You create the application first, then attach your number to it.
- In the Vobiz dashboard, go to Applications
- Create a new application
- Set the Answer URL to
https://xxxx.ngrok-free.app/answer(method:POST) - Set the Hangup URL to
https://xxxx.ngrok-free.app/hangup(method:POST) - Save the application
- Go to your number’s settings and attach it to the application you just created
Step 6 is easy to miss but required. Without a number attached to the application, calls have nowhere to route to, even if the application itself is configured correctly.
8. Run and Test Your Agent
Call your Vobiz number from any phone. Vobiz fetches /answer, opens the WebSocket at /ws, and your agent greets the caller.
Audio Formats
The two directions of a Vobiz call are independent and configured separately.
Vobiz → your agent, set by <Stream contentType>:
Your agent → Vobiz, set per playAudio message: Linear16 at 8000, 16000, or 24000 Hz. 24 kHz applies to playback only, not inbound audio.
playAudio payloads must be raw mono little-endian Linear16, Base64-encoded, with no WAV header. Declaring a sampleRate in the playAudio message does not resample the bytes you send. If you declare 24000 while the payload is actually 8 kHz audio, the caller hears distorted, sped-up “chipmunk” audio. raw_linear16() in Step 4 strips a WAV container if Sarvam happens to return one, but it doesn’t resample.
Read start.mediaFormat on the start event rather than assuming a rate, as this guide’s _handle_start does. Vobiz tells you the exact encoding and sample rate it negotiated for each call.
Customization Examples
Hindi Voice Agent
Multilingual Agent (Auto-detect)
Use AGENT_LANGUAGE = "unknown" for support lines where callers might speak any of several languages. Sarvam detects the spoken language per utterance, so the same agent can handle a Hindi caller followed by a Tamil caller without any code changes.
Available Language Codes
Speaker Voices (Bulbul v3)
Male: Anand (default here), Aditya, Rahul, Rohan, Amit, Dev, Ratan, Varun, Manan, Sumit, Kabir, Aayan, Ashutosh, Advait, Tarun, Sunny, Mani, Gokul, Vijay, Mohit, Rehan, Soham, Shubh
Female: Ritu, Priya, Neha, Pooja, Simran, Kavya, Ishita, Shreya, Roopa, Tanya, Shruti, Suhani, Kavitha, Rupali
Speakers are model-specific. A bulbul:v2 voice such as anushka is rejected by bulbul:v3. Check that the speaker you pick is valid for the SARVAM_TTS_MODEL you’re using.
Understanding the Call Flow
- Caller dials your Vobiz number. Vobiz fetches your Answer URL, and executes the returned
<Stream>XML, opening a bidirectional WebSocket to your agent. startevent: Vobiz sends the stream ID and negotiated audio format. Your agent speaks a greeting.mediaevents: Caller audio arrives as base64-encoded chunks. An energy-based VAD buffers speech and detects when the caller has stopped talking.- STT: The buffered utterance is sent to Sarvam’s Saaras model for transcription.
- LLM: The transcript is added to the conversation and streamed through a Sarvam chat model.
- TTS: Each completed sentence of the reply is synthesized with Sarvam’s Bulbul and sent back as
playAudioevents, so playback starts before the whole reply is ready. - Barge-in: If the caller speaks while the agent is still playing audio, a
clearAudioevent stops playback and a new utterance begins.
Pro Tips
- Use
AGENT_LANGUAGE = "unknown"to auto-detect the caller’s language on multilingual support lines. - Sarvam’s models understand code-mixing, so your agent can naturally handle Hinglish, Tanglish, and other mixed languages common on real support calls.
- Keep the system prompt explicit about reply length and formatting. Since the reply is read aloud, ask the model to avoid markdown, bullet points, and emoji.
- If a chat completion ever returns empty content (the model spent its whole token budget reasoning), have the agent speak a fallback line instead of going silent.
Log every transcript and reply to a file or database for post-call analytics. It’s cheap and doesn’t slow down the live pipeline.
Troubleshooting
Call connects but there’s no audio. Confirm PUBLIC_URL in .env matches your current ngrok URL exactly, and that the Answer URL configured on your Vobiz application points at /answer on that same URL.
Call doesn’t reach your agent at all. Confirm your number is actually attached to the application, not just that the application has the right URLs. A number with no application attached has nowhere to route an incoming call.
Choppy or chipmunk-pitched audio. Check that OUTBOUND_SAMPLE_RATE and the sampleRate you declare in each playAudio message match the actual bytes you’re sending. Declaring a rate that doesn’t match the payload distorts pitch and speed without erroring.
Playback has a burst of noise at the start. Sarvam TTS occasionally returns a WAV-wrapped file instead of raw PCM. Make sure raw_linear16() runs on every synthesized chunk before it’s sent as playAudio.
API key errors. Make sure SARVAM_API_KEY is in your .env file and that the file sits next to server.py.
Empty or truncated replies. sarvam-105b spends tokens on internal reasoning before writing the reply. Raise max_tokens on the chat request; 1500 to 2000 is a safe starting point.
Barge-in doesn’t trigger, or triggers on background noise. The VAD here is a simple RMS energy threshold, not real speech detection. Raise SILENCE_THRESHOLD on noisy lines, or set it high enough to ignore ambient noise from the caller’s environment.
Additional Resources
Need Help?
- Sarvam Support: developer@sarvam.ai
- Community: Join the Discord Community
Happy Building!