> 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.

# Migrating Text-to-Speech from ElevenLabs to Sarvam

> Endpoint, parameter, and response-format differences between ElevenLabs and Sarvam's Text-to-Speech API, with before/after code for REST, streaming, and pronunciation dictionaries.

This guide covers migrating ElevenLabs text-to-speech API calls to Sarvam. If you're building a full voice agent rather than replacing individual TTS calls, start from the [LiveKit](/api-reference-docs/integration/build-voice-agent-with-live-kit) or [Pipecat](/api-reference-docs/integration/build-voice-agent-with-pipecat) voice agent build guides instead: they aren't a 1:1 API swap and aren't covered here.

**In short:** swap `voice_id` for `speaker`, add a required `target_language_code`, and change your response handling: ElevenLabs returns raw audio bytes, Sarvam returns JSON with base64-encoded audio in an `audios` array. Sarvam's structured response is what makes it easy to pair audio with per-request metadata; that decode step is the change most migrations look for first.

## Quick start

Base URL and auth header change the same way for every ElevenLabs migration, so they're folded in here rather than in a separate page:

|                     | ElevenLabs                  | Sarvam                  |
| ------------------- | --------------------------- | ----------------------- |
| Base URL            | `https://api.elevenlabs.io` | `https://api.sarvam.ai` |
| Auth header         | `xi-api-key`                | `api-subscription-key`  |
| Auth failure status | `401`                       | `403`                   |

With that, replace the synthesis call and add the required language code:

```python
from elevenlabs import ElevenLabs

client = ElevenLabs(api_key="YOUR_ELEVENLABS_API_KEY")

audio = client.text_to_speech.convert(
    voice_id="21m00Tcm4TlvDq8ikWAM",
    model_id="eleven_multilingual_v2",
    text="Welcome to our platform!",
    voice_settings={
        "stability": 0.5,
        "similarity_boost": 0.75,
        "style": 0.0,
        "use_speaker_boost": True,
        "speed": 1.0,
    },
)

with open("output.mp3", "wb") as f:
    for chunk in audio:
        f.write(chunk)
```

```javascript
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
import fs from "fs";

const client = new ElevenLabsClient({ apiKey: "YOUR_ELEVENLABS_API_KEY" });

const audio = await client.textToSpeech.convert("21m00Tcm4TlvDq8ikWAM", {
  text: "Welcome to our platform!",
  modelId: "eleven_multilingual_v2",
  voiceSettings: {
    stability: 0.5,
    similarityBoost: 0.75,
    style: 0.0,
    useSpeakerBoost: true,
    speed: 1.0,
  },
});

const reader = audio.getReader();
const chunks = [];
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  chunks.push(value);
}
fs.writeFileSync("output.mp3", Buffer.concat(chunks.map((c) => Buffer.from(c))));
```

```bash
curl -X POST "https://api.elevenlabs.io/v1/text-to-speech/21m00Tcm4TlvDq8ikWAM" \
  -H "xi-api-key: YOUR_ELEVENLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Welcome to our platform!",
    "model_id": "eleven_multilingual_v2",
    "voice_settings": {"stability": 0.5, "similarity_boost": 0.75}
  }' \
  --output output.mp3
```

```python
import base64
from sarvamai import SarvamAI

client = SarvamAI(api_subscription_key="YOUR_SARVAM_API_KEY")

response = client.text_to_speech.convert(
    text="Welcome to our platform!",
    target_language_code="en-IN",
    speaker="shubh",
    model="bulbul:v3",
    pace=1.0,
)

with open("output.wav", "wb") as f:
    f.write(base64.b64decode("".join(response.audios)))
```

```javascript
import { SarvamAIClient } from "sarvamai";
import fs from "fs";

const client = new SarvamAIClient({ apiSubscriptionKey: "YOUR_SARVAM_API_KEY" });

const response = await client.textToSpeech.convert({
  text: "Welcome to our platform!",
  target_language_code: "en-IN",
  speaker: "shubh",
  model: "bulbul:v3",
  pace: 1.0,
});

fs.writeFileSync("output.wav", Buffer.from(response.audios.join(""), "base64"));
```

```bash
curl -X POST https://api.sarvam.ai/text-to-speech \
  -H "api-subscription-key: YOUR_SARVAM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Welcome to our platform!",
    "target_language_code": "en-IN",
    "speaker": "shubh",
    "model": "bulbul:v3"
  }'
```

ElevenLabs' Python and JavaScript SDKs hand back an iterable/stream of audio chunks you can write directly. Sarvam's `convert()` returns a JSON object; the audio lives in `response.audios` as a list of base64 strings, one per input text. Join and decode before writing to disk, or use the Python SDK's `sarvamai.play.save()` helper, which does this for you.

The REST response is JSON with a base64 `audios` array, not a file. Decode `.audios[0]` before writing it out; see the [REST API guide](/api-reference-docs/api-guides-tutorials/text-to-speech/rest-api) for the full decode snippet.

The rest of this page covers the full endpoint and parameter mapping, streaming, pronunciation dictionaries, and common mistakes.

## Endpoint map

The tables below aren't a checklist to apply field-by-field: Quick Start above already shows the whole call swapped over in one go. Use these tables as a reference for what changed and why, not as a sequence of edits to make yourself.

|                       | ElevenLabs                                                | Sarvam                                                                                                                                  |
| --------------------- | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| Synchronous synthesis | `POST /v1/text-to-speech/{voice_id}`                      | `POST /text-to-speech`                                                                                                                  |
| HTTP streaming        | `POST /v1/text-to-speech/{voice_id}/stream`               | `POST /text-to-speech/stream`                                                                                                           |
| WebSocket streaming   | `GET wss://.../v1/text-to-speech/{voice_id}/stream-input` | `GET /text-to-speech/ws`                                                                                                                |
| Voice discovery       | `GET /v2/voices` (paginated, searchable)                  | No API call needed; speaker names are a fixed list on the [Voices](/api-reference-docs/api-guides-tutorials/text-to-speech/voices) page |

## Parameter concept map

| ElevenLabs                                                                     | Sarvam                                                                                         | Note                                                                                                                                                               |
| ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `voice_id` (opaque ID, e.g. `21m00Tcm4TlvDq8ikWAM`)                            | `speaker` (plain lowercase name, e.g. `shubh`, `priya`)                                        | No lookup call required, pick a name directly from the [Voices](/api-reference-docs/api-guides-tutorials/text-to-speech/voices) page                               |
| `model_id` (`eleven_multilingual_v2`, `eleven_flash_v2_5`, `eleven_v3`, etc.)  | `model` (`bulbul:v2` or `bulbul:v3`)                                                           | Two model versions total, not a family of speed/quality tiers                                                                                                      |
| `voice_settings.stability`                                                     | Built into the voice                                                                           | Every Bulbul speaker is pre-tuned for consistent delivery, so there's no separate dial to calibrate                                                                |
| `voice_settings.similarity_boost`                                              | Not needed                                                                                     | Sarvam speakers are production-ready studio voices rather than per-request clones, so there's nothing to tune                                                      |
| `voice_settings.style`                                                         | Speaker choice                                                                                 | Pick the speaker whose baseline delivery already matches the expressiveness you want; see [Voices](/api-reference-docs/api-guides-tutorials/text-to-speech/voices) |
| `voice_settings.use_speaker_boost`                                             | Not needed                                                                                     | Clarity is tuned in by default on every speaker                                                                                                                    |
| `voice_settings.speed` (default `1.0`)                                         | `pace` (default `1.0`, range `0.5` to `2.0` on v3, `0.3` to `3.0` on v2)                       | Same idea, wider range on Sarvam's side                                                                                                                            |
| `language_code` (optional, most models infer language from voice/model)        | `target_language_code` (**required** on every request)                                         | Guarantees the right pronunciation and prosody on every call, which matters most for code-mixed Indian-language text                                               |
| `output_format` (single string, e.g. `mp3_44100_128`)                          | `speech_sample_rate` + `output_audio_codec` (separate params, available on REST and streaming) | Defaults to WAV at 24000 Hz; set `output_audio_codec` (`mp3`, `opus`, `flac`, `aac`, `wav`, etc.) and `speech_sample_rate` independently to change either          |
| `pronunciation_dictionary_locators`                                            | `dict_id`                                                                                      | Both are scoped and reusable across requests, format differs, see [Pronunciation dictionary](#pronunciation-dictionary) below                                      |
| Response: raw binary audio (`application/octet-stream` or per `output_format`) | Response: JSON, audio as base64 strings in `audios`                                            | Structured response makes it straightforward to pair audio with per-request metadata; decode `.audios[0]` before writing to disk                                   |

`pitch` and `loudness` give you fine-grained tone and volume control on **`bulbul:v2`**. On **`bulbul:v3`**, which adds a larger voice set and stronger code-mixed handling, tone comes from speaker choice instead, so pick a speaker whose baseline delivery already fits rather than reaching for these two params.

## Streaming

|                  | ElevenLabs                                                                                                                                         | Sarvam                                                                                                                                                                                                                                    |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| HTTP streaming   | `POST /v1/text-to-speech/{voice_id}/stream`                                                                                                        | [HTTP Stream API](/api-reference-docs/api-guides-tutorials/text-to-speech/streaming-api/http-stream) (`POST /text-to-speech/stream`): both return audio progressively over one request, no persistent connection                          |
| WebSocket        | `.../stream-input`: initial message carrying `voice_settings`, then `SendText` messages (optional `flush` flag), closed with an empty-text message | [WebSocket API](/api-reference-docs/api-guides-tutorials/text-to-speech/streaming-api/web-socket): `ws.configure(...)` once, then repeated `ws.convert(text)` and `ws.flush()` on the same connection                                     |
| Character limits | A single large call, e.g. up to 10,000 characters on `eleven_multilingual_v2`                                                                      | WebSocket up to 2500 chars/message (under 500 recommended for lowest latency), REST up to 2500 on `bulbul:v3` (1500 on `bulbul:v2`), HTTP streaming up to 3500 regardless of model — may need chunking logic that didn't previously exist |

## Pronunciation dictionary

Both platforms let you fix mispronounced brand names and acronyms without retraining a model, but the mechanism differs:

|                   | ElevenLabs                                                        | Sarvam                                                                                                              |
| ----------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| Rule types        | Phoneme rules (IPA) and word-alias rules                          | Word-alias (plain text substitution), simple to author and audit                                                    |
| Scope             | Referenced via `pronunciation_dictionary_locators` in the request | Referenced via a single `dict_id` per request                                                                       |
| Language handling | Rules can be tied to specific words regardless of language        | Rules are nested per `target_language_code`; only entries matching the request's language apply                     |
| Limits            | Varies by plan                                                    | 10 dictionaries per user, 100 words per dictionary, 1 MB per upload, 1 dictionary per request                       |
| Model support     | Available across ElevenLabs' model tiers                          | **`bulbul:v3` only**: a request can't combine a pronunciation dictionary with the v2-only `pitch`/`loudness` params |

If your ElevenLabs dictionary relies on IPA phoneme rules for precise pronunciation control, plan to rewrite it: Sarvam dictionaries use plain word-to-word text replacements (e.g. `"NAIC": "N A I C"`), which are simpler to author and audit than phonetic transcriptions. See the [Pronunciation Dictionary guide](/api-reference-docs/api-guides-tutorials/text-to-speech/pronunciation-dictionary) for the full format and upload flow.

## What's different by design

* **Ready-to-use voices, no cloning step.** Sarvam ships a curated, named set of 30+ Bulbul v3 voices, professionally tuned and ready to call immediately, with no per-request cloning or training step to manage.
* **Explicit language targeting.** Every Sarvam request carries `target_language_code`, so pronunciation and prosody are correct by design on every call, especially valuable for Hindi, Hinglish, and other code-mixed Indian-language text where auto-detection is prone to guessing wrong.
* **A simpler tuning surface.** Tone and delivery come from picking the right speaker plus `pace` (and `pitch`/`loudness` on v2), instead of calibrating `stability`, `similarity_boost`, `style`, and `use_speaker_boost` separately. Fewer knobs, less trial and error to land on a voice that sounds right.
* **Indian language depth is Sarvam's specific strength.** Sarvam covers Hindi plus 9 other Indian languages (Bengali, Tamil, Telugu, Kannada, Malayalam, Marathi, Gujarati, Punjabi, Odia), natively tuned for code-mixed input like Hinglish. If your product needs those languages, this is the reason to migrate, not just a side effect of it.
* **Instant voice discovery.** Every speaker is listed with audio previews on the [Voices](/api-reference-docs/api-guides-tutorials/text-to-speech/voices) page: no API call, no pagination, nothing to query.

## Full example

A small wrapper function, before and after, showing the complete set of changes together:

```python
from elevenlabs import ElevenLabs

client = ElevenLabs(api_key="YOUR_ELEVENLABS_API_KEY")

def synthesize(text: str, out_path: str):
    audio = client.text_to_speech.convert(
        voice_id="21m00Tcm4TlvDq8ikWAM",
        model_id="eleven_multilingual_v2",
        text=text,
        voice_settings={"stability": 0.5, "similarity_boost": 0.75},
    )
    with open(out_path, "wb") as f:
        for chunk in audio:
            f.write(chunk)
```

```javascript
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
import fs from "fs";

const client = new ElevenLabsClient({ apiKey: "YOUR_ELEVENLABS_API_KEY" });

async function synthesize(text, outPath) {
  const audio = await client.textToSpeech.convert("21m00Tcm4TlvDq8ikWAM", {
    text,
    modelId: "eleven_multilingual_v2",
    voiceSettings: { stability: 0.5, similarityBoost: 0.75 },
  });

  const reader = audio.getReader();
  const chunks = [];
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    chunks.push(value);
  }
  fs.writeFileSync(outPath, Buffer.concat(chunks.map((c) => Buffer.from(c))));
}
```

```bash
synthesize() {
  local text="$1" out_path="$2"
  curl -sS -X POST "https://api.elevenlabs.io/v1/text-to-speech/21m00Tcm4TlvDq8ikWAM" \
    -H "xi-api-key: YOUR_ELEVENLABS_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{\"text\": \"$text\", \"model_id\": \"eleven_multilingual_v2\", \"voice_settings\": {\"stability\": 0.5, \"similarity_boost\": 0.75}}" \
    --output "$out_path"
}
```

```python
import base64
from sarvamai import SarvamAI
from sarvamai.core.api_error import ApiError

client = SarvamAI(api_subscription_key="YOUR_SARVAM_API_KEY")

def synthesize(text: str, out_path: str, language_code: str = "en-IN") -> None:
    try:
        response = client.text_to_speech.convert(
            text=text,
            target_language_code=language_code,
            speaker="shubh",
            model="bulbul:v3",
        )
    except ApiError as e:
        if e.status_code == 403:
            raise RuntimeError("Invalid API key. Check your credentials.") from e
        elif e.status_code == 429:
            raise RuntimeError("Rate limit exceeded. Wait and retry.") from e
        elif e.status_code == 503:
            raise RuntimeError("Service overloaded. Retry with backoff.") from e
        raise

    with open(out_path, "wb") as f:
        f.write(base64.b64decode("".join(response.audios)))
```

```javascript
import { SarvamAIClient, SarvamAIError } from "sarvamai";
import fs from "fs";

const client = new SarvamAIClient({ apiSubscriptionKey: "YOUR_SARVAM_API_KEY" });

async function synthesize(text, outPath, languageCode = "en-IN") {
  let response;
  try {
    response = await client.textToSpeech.convert({
      text,
      target_language_code: languageCode,
      speaker: "shubh",
      model: "bulbul:v3",
    });
  } catch (e) {
    if (e instanceof SarvamAIError) {
      if (e.statusCode === 403) throw new Error("Invalid API key. Check your credentials.");
      if (e.statusCode === 429) throw new Error("Rate limit exceeded. Wait and retry.");
      if (e.statusCode === 503) throw new Error("Service overloaded. Retry with backoff.");
    }
    throw e;
  }
  fs.writeFileSync(outPath, Buffer.from(response.audios.join(""), "base64"));
}
```

```bash
synthesize() {
  local text="$1" out_path="$2" language_code="${3:-en-IN}"
  local body status audio_b64
  body=$(curl -sS -w '\n%{http_code}' -X POST https://api.sarvam.ai/text-to-speech \
    -H "api-subscription-key: YOUR_SARVAM_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{\"text\": \"$text\", \"target_language_code\": \"$language_code\", \"speaker\": \"shubh\", \"model\": \"bulbul:v3\"}")
  status="${body##*$'\n'}"
  case "$status" in
    403) echo "Invalid API key. Check your credentials." >&2; return 1 ;;
    429) echo "Rate limit exceeded. Wait and retry." >&2; return 1 ;;
    503) echo "Service overloaded. Retry with backoff." >&2; return 1 ;;
  esac
  # Minimal single-value extraction; use a real JSON parser for production code.
  audio_b64=$(echo "${body%$'\n'*}" | grep -o '"audios":\["[^"]*"' | cut -d'"' -f4)
  echo "$audio_b64" | base64 -d > "$out_path"
}
```

## Common mistakes

The single most common migration bug. ElevenLabs' REST response is the audio file itself; Sarvam's is JSON. Code that does `f.write(response.content)` unmodified will produce a file full of JSON text, not audio. Decode `response.audios[0]` from base64 first.

It's required on every request, by design: this is what guarantees Sarvam applies the correct pronunciation and prosody instead of guessing from the `speaker` name. Include it from the start rather than treating it as optional.

`pace` and `pitch` aren't drop-in replacements for `stability` or `style`. Rather than compensating with those params, pick a speaker whose baseline delivery already matches what you need; see the [Voices](/api-reference-docs/api-guides-tutorials/text-to-speech/voices) page for per-speaker tone descriptions.

Both params are accepted syntactically but only take effect on `bulbul:v2`. If output sounds unchanged after setting them, confirm which model version the request is actually using.

Sarvam's dictionary format is plain word-to-word substitution, not phoneme rules. An ElevenLabs dictionary built on IPA transcriptions needs to be rewritten as text replacements, not just re-uploaded.

## See also

* [Migration overview](/api-reference-docs/migrations/from-elevenlabs/overview): base URL, auth, and SDK client changes
* [Migrating Speech-to-Text](/api-reference-docs/migrations/from-elevenlabs/speech-to-text)
* [Which Text-to-Speech API to Use](/api-reference-docs/api-guides-tutorials/text-to-speech/which-api-to-use)
* [TTS Best Practices](/api-reference-docs/api-guides-tutorials/text-to-speech/best-practices)
* [Voices](/api-reference-docs/api-guides-tutorials/text-to-speech/voices)
* [Pronunciation Dictionary](/api-reference-docs/api-guides-tutorials/text-to-speech/pronunciation-dictionary)
* [Errors & Troubleshooting](/api-reference-docs/errors-troubleshooting)