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

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

This guide covers migrating Cartesia 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 the `voice` object for a plain `speaker` name, drop the nested `output_format` object for two flat params, and change your response handling: Cartesia 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 Cartesia migration, so they're folded in here rather than in a separate page:

|                     | Cartesia                          | Sarvam                            |
| ------------------- | --------------------------------- | --------------------------------- |
| Base URL            | `https://api.cartesia.ai`         | `https://api.sarvam.ai`           |
| Auth header         | `Authorization: Bearer <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 cartesia import Cartesia

client = Cartesia(api_key="YOUR_CARTESIA_API_KEY")

response = client.tts.generate(
    model_id="sonic-3.5",
    transcript="Welcome to our platform!",
    voice={
        "mode": "id",
        "id": "e07c00bc-4134-4eae-9ea4-1a55fb45746b",
    },
    output_format={
        "container": "wav",
        "encoding": "pcm_f32le",
        "sample_rate": 44100,
    },
)

response.write_to_file("output.wav")
```

```javascript
import { CartesiaClient } from "@cartesia/cartesia-js";
import fs from "fs";

const client = new CartesiaClient({ apiKey: "YOUR_CARTESIA_API_KEY" });

const response = await client.tts.generate({
  model_id: "sonic-3.5",
  transcript: "Welcome to our platform!",
  voice: { mode: "id", id: "e07c00bc-4134-4eae-9ea4-1a55fb45746b" },
  output_format: { container: "wav", encoding: "pcm_f32le", sample_rate: 44100 },
});

fs.writeFileSync("output.wav", Buffer.from(await response.arrayBuffer()));
```

```bash
curl -X POST https://api.cartesia.ai/tts/bytes \
  -H "Authorization: Bearer YOUR_CARTESIA_API_KEY" \
  -H "Cartesia-Version: 2026-03-01" \
  -H "Content-Type: application/json" \
  -d '{
    "model_id": "sonic-3.5",
    "transcript": "Welcome to our platform!",
    "voice": {"mode": "id", "id": "e07c00bc-4134-4eae-9ea4-1a55fb45746b"},
    "output_format": {"container": "wav", "encoding": "pcm_f32le", "sample_rate": 44100}
  }' \
  --output output.wav
```

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

Cartesia's `generate()` returns a binary response with a `write_to_file()` (Python) or `arrayBuffer()` (JavaScript) helper. 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.

|                       | Cartesia                              | Sarvam                                                                                                                                  |
| --------------------- | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| Synchronous synthesis | `POST /tts/bytes`                     | `POST /text-to-speech`                                                                                                                  |
| HTTP streaming        | `POST /tts/sse`                       | `POST /text-to-speech/stream`                                                                                                           |
| WebSocket streaming   | `POST /tts/websocket` (upgrade)       | `GET /text-to-speech/ws`                                                                                                                |
| Voice discovery       | `GET /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

| Cartesia                                                                                                                               | Sarvam                                                                       | Note                                                                                                                                                       |
| -------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `model_id` (`sonic-3.5`, `sonic-3`, or dated pins like `sonic-3-2026-01-12`)                                                           | `model` (`bulbul:v2` or `bulbul:v3`)                                         | One current model to pick, not a family of dated version pins                                                                                              |
| `voice` (object, e.g. `{"mode": "id", "id": "<uuid>"}`)                                                                                | `speaker` (plain lowercase name, e.g. `shubh`, `priya`)                      | No object to construct or lookup call to make; pick a name directly from the [Voices](/api-reference-docs/api-guides-tutorials/text-to-speech/voices) page |
| `output_format` (object, e.g. `{"container": "wav", "encoding": "pcm_f32le", "sample_rate": 44100}`)                                   | `speech_sample_rate` + `output_audio_codec` (flat params)                    | Same two settings, passed directly instead of nested in an object                                                                                          |
| `generation_config.emotion` (`sonic-3`+; dozens of named emotions)                                                                     | Speaker choice                                                               | Pick the speaker whose baseline delivery already carries the tone you want; see [Voices](/api-reference-docs/api-guides-tutorials/text-to-speech/voices)   |
| `generation_config.speed` (float `0.6`-`1.5`, `sonic-3`+); the top-level `speed` (`slow`/`normal`/`fast`) is deprecated in favor of it | `pace` (float, `0.5`-`2.0` on v3, `0.3`-`3.0` on v2)                         | One continuous dial across every model, with no deprecated alternative to avoid                                                                            |
| `pronunciation_dict_id` (`sonic-3`+ only)                                                                                              | `dict_id` (`bulbul:v3` only)                                                 | Same concept, and both platforms restrict it to their latest model generation                                                                              |
| `language` (two-letter code, e.g. `hi`; optional)                                                                                      | `target_language_code` (BCP-47, e.g. `hi-IN`; **required** on every request) | Guarantees the right pronunciation and prosody on every call, which matters most for code-mixed Indian-language text                                       |
| Response: raw binary audio (`BinaryAPIResponse`, via `.write_to_file()`)                                                               | 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

|                            | Cartesia                                                                                                                                                                                                                                                       | Sarvam                                                                                                                                                                                                                                                                                                                                             |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| HTTP streaming             | `POST /tts/sse` (server-sent events): `chunk` events (base64 audio in `data`), a final `done` event, or an `error` event on failure                                                                                                                            | [HTTP Stream API](/api-reference-docs/api-guides-tutorials/text-to-speech/streaming-api/http-stream) (`POST /text-to-speech/stream`): audio returned progressively over one request the same way, with no persistent connection                                                                                                                    |
| WebSocket                  | `POST /tts/websocket` upgrade, every message tied to a `context_id`: `"continue": true` on all but the last chunk of text, then `{"context_id": ..., "flush": true}` to force out buffered audio or `{"context_id": ..., "cancel": true}` to stop a generation | [WebSocket API](/api-reference-docs/api-guides-tutorials/text-to-speech/streaming-api/web-socket): `ws.configure(...)` once, then repeated `ws.convert(text)` (equivalent to a `continue` chunk) and `ws.flush()` (equivalent to `flush`) on the same connection, with no separate context ID to track since one connection is already one context |
| Tone/speed/volume controls | `generation_config` (emotion, speed, volume), `sonic-3` and newer only                                                                                                                                                                                         | `pace` (and `pitch`/`loudness` on `bulbul:v2`), the same way whether you're on REST, HTTP streaming, or WebSocket                                                                                                                                                                                                                                  |

## Pronunciation dictionary

Both platforms use plain text-to-alias substitution rather than phonetic transcription, so the underlying concept carries over directly:

|                   | Cartesia                                                    | Sarvam                                                                                          |
| ----------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| Rule format       | `{"text": "original", "alias": "replacement"}` pairs        | Word-alias (plain text substitution), the same idea                                             |
| Scope             | Referenced via a single `pronunciation_dict_id` per request | Referenced via a single `dict_id` per request                                                   |
| Language handling | One dictionary applies across the request's language        | Rules are nested per `target_language_code`; only entries matching the request's language apply |
| Model support     | `sonic-3` and newer only                                    | `bulbul:v3` only                                                                                |

Since both dictionaries are simple text replacements, porting entries over is mostly a data migration, not a rewrite: copy each `text`/`alias` pair into Sarvam's format and nest it under the right `target_language_code`. 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.
* **Plain speaker names, no voice object.** Sarvam's `speaker` is a lowercase name you pick straight from the [Voices](/api-reference-docs/api-guides-tutorials/text-to-speech/voices) page, rather than a `{"mode": "id", "id": "<uuid>"}` object you construct or fetch from `/voices` first.
* **Flat output settings.** `speech_sample_rate` and `output_audio_codec` are top-level params rather than a nested `output_format` object, so there's less request-building code either way.
* **One continuous pace control.** `pace` works the same way across `bulbul:v2` and `bulbul:v3`, rather than switching between a `slow`/`normal`/`fast` enum on older models and a continuous `0.6`-`1.5` float on newer ones.
* **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.
* **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, alongside dedicated `verbatim`, `translit`, and `codemix` modes on the speech-to-text side.

## Full example

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

```python
from cartesia import Cartesia

client = Cartesia(api_key="YOUR_CARTESIA_API_KEY")

def synthesize(text: str, out_path: str) -> None:
    response = client.tts.generate(
        model_id="sonic-3.5",
        transcript=text,
        voice={"mode": "id", "id": "e07c00bc-4134-4eae-9ea4-1a55fb45746b"},
        output_format={"container": "wav", "encoding": "pcm_f32le", "sample_rate": 44100},
    )
    response.write_to_file(out_path)
```

```javascript
import { CartesiaClient } from "@cartesia/cartesia-js";
import fs from "fs";

const client = new CartesiaClient({ apiKey: "YOUR_CARTESIA_API_KEY" });

async function synthesize(text, outPath) {
  const response = await client.tts.generate({
    model_id: "sonic-3.5",
    transcript: text,
    voice: { mode: "id", id: "e07c00bc-4134-4eae-9ea4-1a55fb45746b" },
    output_format: { container: "wav", encoding: "pcm_f32le", sample_rate: 44100 },
  });
  fs.writeFileSync(outPath, Buffer.from(await response.arrayBuffer()));
}
```

```bash
synthesize() {
  local text="$1" out_path="$2"
  curl -sS -X POST https://api.cartesia.ai/tts/bytes \
    -H "Authorization: Bearer YOUR_CARTESIA_API_KEY" \
    -H "Cartesia-Version: 2026-03-01" \
    -H "Content-Type: application/json" \
    -d "{\"model_id\": \"sonic-3.5\", \"transcript\": \"$text\", \"voice\": {\"mode\": \"id\", \"id\": \"e07c00bc-4134-4eae-9ea4-1a55fb45746b\"}, \"output_format\": {\"container\": \"wav\", \"encoding\": \"pcm_f32le\", \"sample_rate\": 44100}}" \
    --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. Cartesia's 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 relying on the optional, best-effort `language` Cartesia infers from the voice. Include it from the start rather than treating it as optional.

There isn't a direct swap for `generation_config.emotion`, and that's fine: pick a speaker whose baseline delivery already matches what you need instead of trying to force `pace` to compensate; see [Voices](/api-reference-docs/api-guides-tutorials/text-to-speech/voices) for per-speaker tone descriptions.

Like Cartesia's own `sonic-3` restriction, Sarvam's `dict_id` only applies on `bulbul:v3`. If you migrate a dictionary-dependent request onto `bulbul:v2`, the dictionary is silently ignored.

Cartesia's dated pins (e.g. `sonic-3-2026-01-12`) don't have a Sarvam equivalent to match one-to-one. Map them to the corresponding model family instead: any `sonic-3*` pin to `bulbul:v3`, any older pin to `bulbul:v2`.

## See also

* [Migration overview](/api-reference-docs/migrations/from-cartesia/overview): base URL, auth, and SDK client changes
* [Migrating Speech-to-Text](/api-reference-docs/migrations/from-cartesia/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)