Migrating Text-to-Speech from Cartesia to Sarvam

View as Markdown

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 or 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:

CartesiaSarvam
Base URLhttps://api.cartesia.aihttps://api.sarvam.ai
Auth headerAuthorization: Bearer <api_key>api-subscription-key: <api_key>
Auth failure status401403

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

1from cartesia import Cartesia
2
3client = Cartesia(api_key="YOUR_CARTESIA_API_KEY")
4
5response = client.tts.generate(
6 model_id="sonic-3.5",
7 transcript="Welcome to our platform!",
8 voice={
9 "mode": "id",
10 "id": "e07c00bc-4134-4eae-9ea4-1a55fb45746b",
11 },
12 output_format={
13 "container": "wav",
14 "encoding": "pcm_f32le",
15 "sample_rate": 44100,
16 },
17)
18
19response.write_to_file("output.wav")

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

CartesiaSarvam
Synchronous synthesisPOST /tts/bytesPOST /text-to-speech
HTTP streamingPOST /tts/ssePOST /text-to-speech/stream
WebSocket streamingPOST /tts/websocket (upgrade)GET /text-to-speech/ws
Voice discoveryGET /voices (paginated, searchable)No API call needed; speaker names are a fixed list on the Voices page

Parameter concept map

CartesiaSarvamNote
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 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 choicePick the speaker whose baseline delivery already carries the tone you want; see Voices
generation_config.speed (float 0.6-1.5, sonic-3+); the top-level speed (slow/normal/fast) is deprecated in favor of itpace (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 audiosStructured response makes it straightforward to pair audio with per-request metadata; decode .audios[0] before writing to disk

Streaming

CartesiaSarvam
HTTP streamingPOST /tts/sse (server-sent events): chunk events (base64 audio in data), a final done event, or an error event on failureHTTP Stream API (POST /text-to-speech/stream): audio returned progressively over one request the same way, with no persistent connection
WebSocketPOST /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 generationWebSocket API: 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 controlsgeneration_config (emotion, speed, volume), sonic-3 and newer onlypace (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:

CartesiaSarvam
Rule format{"text": "original", "alias": "replacement"} pairsWord-alias (plain text substitution), the same idea
ScopeReferenced via a single pronunciation_dict_id per requestReferenced via a single dict_id per request
Language handlingOne dictionary applies across the request’s languageRules are nested per target_language_code; only entries matching the request’s language apply
Model supportsonic-3 and newer onlybulbul: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 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 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:

1from cartesia import Cartesia
2
3client = Cartesia(api_key="YOUR_CARTESIA_API_KEY")
4
5def synthesize(text: str, out_path: str) -> None:
6 response = client.tts.generate(
7 model_id="sonic-3.5",
8 transcript=text,
9 voice={"mode": "id", "id": "e07c00bc-4134-4eae-9ea4-1a55fb45746b"},
10 output_format={"container": "wav", "encoding": "pcm_f32le", "sample_rate": 44100},
11 )
12 response.write_to_file(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 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