> 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 Deepgram to Sarvam

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

This guide covers migrating Deepgram 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/integration/build-voice-agent-with-live-kit) or [Pipecat](/api/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:** split Deepgram's combined `model` string into a separate `model` and `speaker`, and change your response handling: both platforms return audio bytes, but Sarvam wraps them in JSON as a base64 `audios` array rather than handing back a raw stream.

## Quick start

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

|                     | Deepgram                         | Sarvam                            |
| ------------------- | -------------------------------- | --------------------------------- |
| Base URL            | `https://api.deepgram.com`       | `https://api.sarvam.ai`           |
| Auth header         | `Authorization: Token <api_key>` | `api-subscription-key: <api_key>` |
| Auth failure status | `401`                            | `403`                             |

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

```python
from deepgram import DeepgramClient

client = DeepgramClient(api_key="YOUR_DEEPGRAM_API_KEY")

audio = client.speak.v1.audio.generate(
    text="Welcome to our platform!",
    model="aura-2-asteria-en",
)

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

```javascript
import { DeepgramClient } from "@deepgram/sdk";
import fs from "fs";

const client = new DeepgramClient({ apiKey: "YOUR_DEEPGRAM_API_KEY" });

const audio = await client.speak.v1.audio.generate({
  text: "Welcome to our platform!",
  model: "aura-2-asteria-en",
});

fs.writeFileSync("output.mp3", Buffer.from(await audio.arrayBuffer()));
```

```bash
curl -X POST https://api.deepgram.com/v1/speak \
  -H "Authorization: Token YOUR_DEEPGRAM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "Welcome to our platform!"}' \
  --output output.mp3 \
  -G --data-urlencode "model=aura-2-asteria-en"
```

```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"
  }'
```

Deepgram's `generate()` yields raw audio chunks/bytes 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/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, 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.

|                       | Deepgram                                                   | Sarvam                        |
| --------------------- | ---------------------------------------------------------- | ----------------------------- |
| Synchronous synthesis | `POST /v1/speak`                                           | `POST /text-to-speech`        |
| HTTP streaming        | `POST /v1/speak` (same endpoint, streamed as it generates) | `POST /text-to-speech/stream` |
| WebSocket streaming   | `POST /v1/speak` (WebSocket upgrade)                       | `GET /text-to-speech/ws`      |

## Parameter concept map

| Deepgram                                                                                      | Sarvam                                                                   | Note                                                                                                                             |
| --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| `model` (voice, model version, and language combined in one string, e.g. `aura-2-asteria-en`) | `model` (`bulbul:v2`/`bulbul:v3`) + `speaker` (plain name, e.g. `shubh`) | Sarvam splits model and voice into two params, so you don't need a separate combined name for every voice                        |
| `encoding` + `container` + `sample_rate` (three separate output settings)                     | `output_audio_codec` + `speech_sample_rate`                              | Same idea, two settings instead of three; Sarvam's REST response defaults to WAV                                                 |
| `speed` (float, model-dependent range)                                                        | `pace` (float, `0.5`-`2.0` on v3, `0.3`-`3.0` on v2)                     | Same concept, consistent range across REST, HTTP streaming, and WebSocket                                                        |
| `bit_rate` (optional, for compressed formats)                                                 | Not applicable                                                           | Sarvam doesn't expose a separate bitrate control; pick `output_audio_codec` for the compression level you need                   |
| `callback` (webhook URL for async delivery)                                                   | Not applicable                                                           | Sarvam's TTS endpoints respond synchronously or stream in real time; there's no async/webhook mode to configure                  |
| Response: raw audio bytes (`Iterator[bytes]`)                                                 | 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 |

## Streaming

|                          | Deepgram                                                                                                                               | Sarvam                                                                                                                                                                                                                                 |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| HTTP streaming           | Same `POST /v1/speak` request, streamed as it generates                                                                                | [HTTP Stream API](/api/api-guides-tutorials/text-to-speech/streaming-api/http-stream) (`POST /text-to-speech/stream`), same behavior over one request with no persistent connection                                                    |
| WebSocket                | `{"type": "Speak", "text": "..."}` per chunk of text, `{"type": "Flush"}` to force out buffered audio, closed with `{"type": "Close"}` | [WebSocket API](/api/api-guides-tutorials/text-to-speech/streaming-api/web-socket): `ws.configure(...)` once, then repeated `ws.convert(text)` (equivalent to `Speak`) and `ws.flush()` (equivalent to `Flush`) on the same connection |
| Discarding buffered text | `{"type": "Clear"}` discards buffered, not-yet-generated text                                                                          | No separate discard operation, since `ws.flush()` already generates audio for whatever text has been sent rather than leaving it queued                                                                                                |

## What's different by design

* **Plain speaker names, no combined model string.** Sarvam's `speaker` is a name you pick straight from the [Voices](/api/api-guides-tutorials/text-to-speech/voices) page, rather than being baked into a single `model` value like `aura-2-asteria-en` that encodes voice, version, and language together.
* **Two output settings instead of three.** `speech_sample_rate` and `output_audio_codec` cover what Deepgram splits across `encoding`, `container`, and `sample_rate`.
* **No async/webhook mode to configure.** Sarvam's TTS endpoints are always synchronous or streamed live, so there's one less delivery mode to decide between.
* **Indian language depth is Sarvam's specific strength.** Deepgram's Aura-2 voices are mostly English, with a handful in Spanish; 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.

## Full example

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

```python
from deepgram import DeepgramClient

client = DeepgramClient(api_key="YOUR_DEEPGRAM_API_KEY")

def synthesize(text: str, out_path: str) -> None:
    audio = client.speak.v1.audio.generate(
        text=text,
        model="aura-2-asteria-en",
    )
    with open(out_path, "wb") as f:
        for chunk in audio:
            f.write(chunk)
```

```javascript
import { DeepgramClient } from "@deepgram/sdk";
import fs from "fs";

const client = new DeepgramClient({ apiKey: "YOUR_DEEPGRAM_API_KEY" });

async function synthesize(text, outPath) {
  const audio = await client.speak.v1.audio.generate({
    text,
    model: "aura-2-asteria-en",
  });
  fs.writeFileSync(outPath, Buffer.from(await audio.arrayBuffer()));
}
```

```bash
synthesize() {
  local text="$1" out_path="$2"
  curl -sS -X POST https://api.deepgram.com/v1/speak \
    -H "Authorization: Token YOUR_DEEPGRAM_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{\"text\": \"$text\"}" \
    --output "$out_path" \
    -G --data-urlencode "model=aura-2-asteria-en"
}
```

```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. Deepgram's REST response is the audio stream 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 inferring language from a combined model string like Deepgram's. Include it from the start rather than treating it as optional.

There isn't one, and that's fine: pick `output_audio_codec` for the compression and quality tradeoff you need instead of tuning a separate bitrate value.

A value like `aura-2-asteria-en` doesn't map to a single Sarvam field. Split it into `model` (`bulbul:v3`) and `speaker` (a name from the [Voices](/api/api-guides-tutorials/text-to-speech/voices) page) instead of trying to find a one-to-one string replacement.

## See also

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