> For clean Markdown of any page, append `.md` to the page URL.
> For a complete documentation index, see https://docs.sarvam.ai/llms.txt.
> For full documentation content in one file, see https://docs.sarvam.ai/llms-full.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.sarvam.ai/_mcp/server.

# Build Your First Voice Agent using Exotel

> A beginner-friendly guide to building a real-time phone voice agent using Exotel, Pipecat, and Sarvam AI. Support for 11 languages (10 Indian + English) with natural voices and multilingual conversations.

## Overview

This guide shows you how to build a **real-time voice agent that answers phone calls**. It uses **Exotel** for telephony, **Pipecat** to orchestrate the real-time audio pipeline, and **Sarvam AI** for speech-to-text, the LLM, and text-to-speech. It's a great starting point for IVR replacements, customer support lines, and conversational phone agents in Indian languages.

## What You'll Build

A voice agent that can:

* Answer inbound phone calls on an Exotel number
* Listen to callers speaking, in multiple Indian languages
* Understand and process what they say
* Respond back in a natural-sounding voice, over the phone

## Quick Overview

1. Get an API key (Sarvam). Exotel needs no API credentials for this flow.
2. Install packages: `pip install "pipecat-ai[websocket,sarvam]" python-dotenv`
3. Create a `.env` file with your API key
4. Write about 80 lines of Python code
5. Point an Exotel Voicebot Applet at your agent's WebSocket URL
6. Call your Exotel number

***

## Quick Start

### 1. Prerequisites

* Python 3.9 or higher
* [ngrok](https://ngrok.com/download), to expose your local server during development
* An [Exotel](https://my.exotel.com) account with voice streaming enabled and a provisioned ExoPhone
* A [Sarvam AI](https://dashboard.sarvam.ai) API key from your dashboard

Unlike Twilio, Exotel's dial-in flow doesn't require an account SID or auth token in your bot code. Pipecat's `ExotelFrameSerializer` only needs the stream and call identifiers Exotel sends over the WebSocket itself.

### 2. Install Dependencies

```bash
pip install "pipecat-ai[websocket,sarvam]" python-dotenv loguru
```

```bash
pip install pipecat-ai[websocket,sarvam] python-dotenv loguru
```

### 3. Create Environment File

Create a file named `.env` in your project folder:

```env
SARVAM_API_KEY=sk_xxxxxxxxxxxxxxxxxxxxxxxx
```

Replace the placeholder with your real key from the [Sarvam dashboard](https://dashboard.sarvam.ai).

### 4. Write Your Agent

Create `agent.py`:

```python
import os
from dotenv import load_dotenv
from loguru import logger
from pipecat.frames.frames import LLMRunFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
    LLMContextAggregatorPair,
)
from pipecat.runner.types import RunnerArguments
from pipecat.runner.utils import create_transport
from pipecat.services.sarvam.stt import SarvamSTTService
from pipecat.services.sarvam.tts import SarvamTTSService
from pipecat.services.sarvam.llm import SarvamLLMService
from pipecat.transports.websocket.fastapi import FastAPIWebsocketParams

load_dotenv(override=True)

async def bot(runner_args: RunnerArguments):
    """Main bot entry point."""

    # create_transport auto-detects Exotel's WebSocket handshake and builds
    # the matching ExotelFrameSerializer using the stream and call identifiers
    # Exotel sends when it connects. No Exotel credentials are needed here.
    transport = await create_transport(
        runner_args,
        {
            "exotel": lambda: FastAPIWebsocketParams(
                audio_in_enabled=True, audio_out_enabled=True
            ),
        },
    )

    # Initialize AI services
    stt = SarvamSTTService(api_key=os.getenv("SARVAM_API_KEY"))
    tts = SarvamTTSService(api_key=os.getenv("SARVAM_API_KEY"))
    llm = SarvamLLMService(
        api_key=os.getenv("SARVAM_API_KEY"),
        settings=SarvamLLMService.Settings(model="sarvam-105b"),
    )

    # Set up conversation context
    messages = [
        {
            "role": "system",
            "content": "You are a friendly AI phone assistant. Keep your responses brief and conversational.",
        },
    ]
    context = LLMContext(messages)
    context_aggregator = LLMContextAggregatorPair(context)

    # Build pipeline
    pipeline = Pipeline(
        [
            transport.input(),
            stt,
            context_aggregator.user(),
            llm,
            tts,
            transport.output(),
            context_aggregator.assistant(),
        ]
    )

    # Exotel Voice Streaming sends and receives 8kHz mono audio
    task = PipelineTask(
        pipeline,
        params=PipelineParams(
            audio_in_sample_rate=8000,
            audio_out_sample_rate=8000,
        ),
    )

    @transport.event_handler("on_client_connected")
    async def on_client_connected(transport, client):
        logger.info("Caller connected")
        messages.append(
            {"role": "system", "content": "Greet the caller and briefly introduce yourself."}
        )
        await task.queue_frames([LLMRunFrame()])

    @transport.event_handler("on_client_disconnected")
    async def on_client_disconnected(transport, client):
        logger.info("Caller disconnected")
        await task.cancel()

    runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
    await runner.run(task)

if __name__ == "__main__":
    from pipecat.runner.run import main
    main()
```

### 5. Configure Exotel to Reach Your Agent

Exotel is different from Twilio here: it doesn't fetch an XML webhook at call time. Instead, you paste your WebSocket URL directly into a Voicebot Applet inside a call flow, and Exotel opens that connection itself.

**Start ngrok:**

```bash
ngrok http 7860
```

ngrok prints a forwarding URL that looks like `https://xxxx.ngrok-free.app`. Turn it into a `wss://` URL for the next step.

The domain suffix ngrok assigns varies per account — you may see `ngrok-free.app`, `ngrok-free.dev`, or the older `ngrok.io`, regardless of whether you're on macOS, Windows, or Linux. Always use whatever URL ngrok actually prints, not the placeholder shown here.

Free ngrok URLs change every time you restart ngrok. If your agent stops receiving calls, check whether the URL in your Voicebot Applet still matches the one ngrok is currently printing.

**Build the App Bazaar flow:**

1. In the [Exotel dashboard](https://my.exotel.com), go to **App Bazaar**
2. Create a new custom app (or edit an existing one)
3. Add a **Voicebot** applet (not **Stream** or **Passthru**), and set its URL field to `wss://xxxx.ngrok-free.app/ws`
4. Add a **Hangup** applet right after it, so the flow is: **Call Start → Voicebot Applet → Hangup Applet**
5. Save the flow

For production use, Exotel recommends adding a **Passthru** applet between Voicebot and Hangup to capture session metadata (call SID, duration, recording URL) and detect disconnects. It's optional for getting a call working.

**Assign the flow to your number:**

1. Go to **ExoPhones**, and select your number
2. Under **Installed App**, choose the flow you just built
3. Confirm the change when prompted

### 6. Run Your Agent

```bash
python agent.py --transport exotel
```

No `--proxy` flag is needed here since Exotel doesn't fetch a webhook, it just connects straight to `/ws`.

### 7. Test Your Agent

Call your Exotel number from any phone. Your voice agent will pick up and start the conversation.

***

## Customization Examples

### Example 1: Hindi Voice Agent

```python
stt = SarvamSTTService(
    api_key=os.getenv("SARVAM_API_KEY"),
    settings=SarvamSTTService.Settings(
        model="saaras:v3",
        language="hi-IN",  # Hindi
    ),
    mode="transcribe",
)

tts = SarvamTTSService(
    api_key=os.getenv("SARVAM_API_KEY"),
    settings=SarvamTTSService.Settings(
        model="bulbul:v3",
        voice="simran",  # Or: priya, ishita, kavya, aditya, anand, rohan
        target_language_code="hi-IN",
    ),
)

llm = SarvamLLMService(
    api_key=os.getenv("SARVAM_API_KEY"),
    settings=SarvamLLMService.Settings(model="sarvam-105b"),
)
```

`language`, `voice`, and `target_language_code` are `Settings` fields, not constructor keyword arguments. Passing them directly to `SarvamSTTService(...)` or `SarvamTTSService(...)` raises a `TypeError` on current versions of `pipecat-ai`. Only `mode` (STT) and `api_key` stay outside `settings`.

### Example 2: Tamil Voice Agent

```python
stt = SarvamSTTService(
    api_key=os.getenv("SARVAM_API_KEY"),
    settings=SarvamSTTService.Settings(
        model="saaras:v3",
        language="ta-IN",
    ),
    mode="transcribe",
)

tts = SarvamTTSService(
    api_key=os.getenv("SARVAM_API_KEY"),
    settings=SarvamTTSService.Settings(
        model="bulbul:v3",
        voice="shubh",
        target_language_code="ta-IN",
    ),
)

llm = SarvamLLMService(
    api_key=os.getenv("SARVAM_API_KEY"),
    settings=SarvamLLMService.Settings(model="sarvam-105b"),
)
```

### Example 3: Multilingual Agent (Auto-detect)

```python
# Auto-detect the caller's language
stt = SarvamSTTService(
    api_key=os.getenv("SARVAM_API_KEY"),
    settings=SarvamSTTService.Settings(
        model="saaras:v3",
        language="unknown",  # Auto-detects language
    ),
    mode="transcribe",
)

tts = SarvamTTSService(
    api_key=os.getenv("SARVAM_API_KEY"),
    settings=SarvamTTSService.Settings(
        model="bulbul:v3",
        voice="anand",
        target_language_code="en-IN",
    ),
)

llm = SarvamLLMService(
    api_key=os.getenv("SARVAM_API_KEY"),
    settings=SarvamLLMService.Settings(model="sarvam-105b"),
)
```

Use `language="unknown"` for support lines where callers might speak any of several languages. Sarvam auto-detects the spoken language per utterance, so the same agent can handle a Hindi caller followed by a Tamil caller without any code changes.

### Example 4: Speech-to-English Agent (Saaras)

Saarika transcribes speech to text in the same language. Saaras translates speech directly to English text. Use Saaras when the caller speaks an Indian language but you want to process and respond in English.

```python
# Caller speaks Hindi, Saaras converts it to English, the LLM processes it,
# and the agent responds in English.

stt = SarvamSTTService(
    api_key=os.getenv("SARVAM_API_KEY"),
    settings=SarvamSTTService.Settings(model="saaras:v3"),
    mode="translate",  # Speech-to-English translation
)

tts = SarvamTTSService(
    api_key=os.getenv("SARVAM_API_KEY"),
    settings=SarvamTTSService.Settings(
        model="bulbul:v3",
        voice="aditya",
        target_language_code="en-IN",
    ),
)

llm = SarvamLLMService(
    api_key=os.getenv("SARVAM_API_KEY"),
    settings=SarvamLLMService.Settings(model="sarvam-105b"),
)
```

Saaras auto-detects the source language (Hindi, Tamil, and so on) and translates spoken content directly to English text, which makes Indian-language speech usable by English-based LLMs.

***

## Available Options

### Language Codes

| Language        | Code      |
| --------------- | --------- |
| English (India) | `en-IN`   |
| Hindi           | `hi-IN`   |
| Bengali         | `bn-IN`   |
| Tamil           | `ta-IN`   |
| Telugu          | `te-IN`   |
| Gujarati        | `gu-IN`   |
| Kannada         | `kn-IN`   |
| Malayalam       | `ml-IN`   |
| Marathi         | `mr-IN`   |
| Punjabi         | `pa-IN`   |
| Odia            | `od-IN`   |
| Auto-detect     | `unknown` |

### Speaker Voices (Bulbul v3)

**Male (23):** Shubh (default), Aditya, Rahul, Rohan, Amit, Dev, Ratan, Varun, Manan, Sumit, Kabir, Aayan, Ashutosh, Advait, Anand, Tarun, Sunny, Mani, Gokul, Vijay, Mohit, Rehan, Soham

**Female (14):** Ritu, Priya, Neha, Pooja, Simran, Kavya, Ishita, Shreya, Roopa, Tanya, Shruti, Suhani, Kavitha, Rupali

### TTS Additional Parameters

You can customize the TTS service with additional parameters:

```python
tts = SarvamTTSService(
    api_key=os.getenv("SARVAM_API_KEY"),
    settings=SarvamTTSService.Settings(
        model="bulbul:v3",
        voice="shubh",
        target_language_code="en-IN",
        pace=1.0,  # Range: 0.5 to 2.0
    ),
)
```

Exotel Voice Streaming defaults to 8kHz mono audio when the Voicebot Applet's WebSocket URL doesn't specify a rate (this guide's URL doesn't). Exotel also supports 16kHz and 24kHz via a `?sample-rate=` query parameter on that URL, but if you use one, update `audio_in_sample_rate`/`audio_out_sample_rate` in `PipelineParams` to match. You don't need to touch `sample_rate` on `SarvamTTSService` either way: Pipecat resamples the TTS output to match `audio_out_sample_rate` automatically.

***

## Understanding the Call Flow

```mermaid
flowchart LR
    caller(["📞 Caller"]) <--> exotel["Exotel"]
    exotel <-->|"wss://<br />audio"| pipeline

    subgraph pipeline["Your Agent Server (Pipecat)"]
        direction LR
        stt["STT"] --> llm["LLM"] --> tts["TTS"]
    end

    pipeline -.->|HTTPS| sarvam["Sarvam AI<br />Saarika/Saaras · sarvam-30b/105b · Bulbul"]
```

1. **Caller dials your Exotel number.** The App Bazaar flow reaches the Voicebot Applet, and Exotel opens a WebSocket directly to your agent — there's no webhook fetch in between.
2. **Transport Input:** Your Pipecat transport receives the caller's audio over the WebSocket, and hands it to STT.
3. **STT (Speech-to-Text):** Converts audio to text using Sarvam's Saarika or Saaras, and the context aggregator adds it to the conversation context.
4. **LLM:** Generates a response using Sarvam.
5. **TTS (Text-to-Speech):** Converts the response to audio using Sarvam's Bulbul.
6. **Transport Output:** Streams the audio back over the WebSocket, and Exotel plays it to the caller, while the context aggregator saves the assistant's response to context. The **Hangup Applet** then ends the call once your agent closes the WebSocket.

***

## Pro Tips

* Use `language="unknown"` to automatically detect the caller's language. This works well for multilingual support lines.
* Sarvam's models understand code-mixing, so your agent can naturally handle Hinglish, Tanglish, and other mixed languages, which is common on real support calls.
* Use `sarvam-30b` for faster responses, or `sarvam-105b` for more complex conversations.

Log the transcript from `context_aggregator` if you want post-call analytics. Writing it to a file or database is cheap and won't slow down the live pipeline.

***

## Troubleshooting

**Call connects but there's no audio.** Confirm the Voicebot Applet's URL uses `wss://`, not `ws://` or `https://`, and that it points at your current ngrok URL. Free ngrok URLs change on every restart.

**Call doesn't reach your agent at all.** Double-check the flow order in App Bazaar: **Call Start → Voicebot Applet → Hangup Applet**. A missing or misordered applet is the most common cause.

**API key errors.** Make sure `SARVAM_API_KEY` is in your `.env` file and that the file sits in the same directory as `agent.py`.

**Module not found.** Re-run the install command for your operating system (see Step 2 above).

**Poor transcription.** Try `language="unknown"` for auto-detection, or set the exact language code (`en-IN`, `hi-IN`, and so on) if you already know it.

**Choppy or robotic audio.** Make sure `audio_in_sample_rate` and `audio_out_sample_rate` in `PipelineParams` match the rate Exotel is actually streaming: `8000` by default, or whatever you set via `?sample-rate=` on the Voicebot Applet's WebSocket URL (Exotel also supports 16000 and 24000). A mismatch causes distorted playback.

**Call disconnects a few seconds after connecting.** Exotel expects your WebSocket server to accept the connection within about 10 seconds. Make sure ngrok and `agent.py` are both already running before you place the call.

**Long call cuts off unexpectedly.** Exotel caps a single streaming session at around 60 minutes (this can vary by plan) — for calls that need to run longer, design your agent to end the conversation before that limit.

***

## Additional Resources

* [Sarvam AI Documentation](https://docs.sarvam.ai)
* [Pipecat Documentation](https://docs.pipecat.ai)
* [Pipecat Sarvam LLM Service](https://docs.pipecat.ai/api-reference/server/services/llm/sarvam)
* [Exotel Stream and Voicebot Applet Guide](https://support.exotel.com/support/solutions/articles/3000108630-working-with-the-stream-and-voicebot-applet)
* [Exotel Updated Extension Guide (timeouts, session limits, Passthru)](https://support.exotel.com/support/solutions/articles/3000132302-updated-extension-guide-working-with-the-stream-and-voicebot-applet-beta-)
* [Exotel Dashboard](https://my.exotel.com)

***

## Need Help?

* Sarvam Support: [developer@sarvam.ai](mailto:developer@sarvam.ai)
* Community: [Join the Discord Community](https://discord.com/invite/5rAsykttcs)

***

**Happy Building!**