> 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 Speech-to-Text from Cartesia to Sarvam

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

This guide covers migrating Cartesia speech-to-text (transcription) API calls to Sarvam. If you're building a full voice agent rather than transcribing audio directly, 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 `model` for `model`, keep passing your audio file directly, and pick the right Sarvam API for the job: REST for short synchronous requests, Batch if you need diarization or very long audio, and WebSocket for real-time streaming.

## 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 transcription call:

```python
from cartesia import Cartesia

client = Cartesia(api_key="YOUR_CARTESIA_API_KEY")

with open("audio.mp3", "rb") as f:
    transcription = client.stt.transcribe(
        file=f,
        model="ink-whisper",
        language="en",
    )

print(transcription.text)
```

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

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

const transcription = await client.stt.transcribe({
  file: fs.createReadStream("audio.mp3"),
  model: "ink-whisper",
  language: "en",
});

console.log(transcription.text);
```

```bash
curl -X POST https://api.cartesia.ai/stt \
  -H "Authorization: Bearer YOUR_CARTESIA_API_KEY" \
  -H "Cartesia-Version: 2025-04-16" \
  -F model="ink-whisper" \
  -F language="en" \
  -F file=@audio.mp3
```

```python
from sarvamai import SarvamAI

client = SarvamAI(api_subscription_key="YOUR_SARVAM_API_KEY")

response = client.speech_to_text.transcribe(
    file=open("audio.mp3", "rb"),
    model="saaras:v3",
    language_code="en-IN",
    mode="transcribe",
)

print(response.transcript)
```

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

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

const response = await client.speechToText.transcribe({
  file: fs.createReadStream("audio.mp3"),
  model: "saaras:v3",
  language_code: "en-IN",
  mode: "transcribe",
});

console.log(response.transcript);
```

```bash
curl -X POST https://api.sarvam.ai/speech-to-text \
  -H "api-subscription-key: YOUR_SARVAM_API_KEY" \
  -F model="saaras:v3" \
  -F language_code="en-IN" \
  -F mode="transcribe" \
  -F file=@audio.mp3
```

Cartesia's `/stt` endpoint transcribes audio of any length synchronously, with no separate long-audio mode. Sarvam splits this by use case: REST for requests under 30 seconds, and Batch for longer audio or when you need speaker diarization, which Cartesia's STT doesn't offer at all.

The rest of this page covers the full endpoint and parameter mapping, diarization via the Batch API, 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.

|                                         | Cartesia                                                                                     | Sarvam                                                                                                                            |
| --------------------------------------- | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| Transcription (any length, synchronous) | `POST /stt`                                                                                  | `POST /speech-to-text` (up to 30 seconds per request; use Batch for longer audio)                                                 |
| Diarization or very long audio          | Not available as a separate mode                                                             | Batch API: `POST /speech-to-text/job/v1` (create), upload, start, then poll or receive a webhook callback, up to 2 hours per file |
| Real-time streaming transcription       | `POST /stt/turns/websocket` (auto turn detection) or `POST /stt/websocket` (manual finalize) | `GET /speech-to-text/ws`                                                                                                          |

## Parameter concept map

| Cartesia                                                                                            | Sarvam                                                                                                 | Note                                                                                                    |
| --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- |
| `model` (`ink-whisper` for `/stt`; a separate `ink-2` model for both real-time WebSocket endpoints) | `model` (`saaras:v3` everywhere)                                                                       | One model name across REST, Batch, and Streaming, rather than a different model for batch vs. real-time |
| `file` (audio file, any supported format)                                                           | `file` (upload the audio file directly)                                                                | Same concept; both take the file directly rather than a hosted URL                                      |
| `language` (ISO-639-1, e.g. `hi`; optional, defaults to `en`)                                       | `language_code` (BCP-47, e.g. `hi-IN`; optional; pass `unknown` to auto-detect)                        | Same idea; explicit BCP-47 region codes on Sarvam's side (`hi-IN` rather than `hi`)                     |
| `timestamp_granularities` (`["word"]`, REST only)                                                   | Timestamps included automatically on Batch, at chunk (sentence/phrase) granularity                     | See the timestamps note below for the granularity difference                                            |
| Response: `text` + `words[]` (word-level timing)                                                    | Response: `transcript` + `diarized_transcript.entries[]` (Batch, with `speaker_id` and segment timing) | Field names differ; see the timestamps note below                                                       |

Sarvam's Batch API returns timestamps per chunk (sentence or phrase segment), which is precise enough for subtitle alignment and audio navigation. If your Cartesia integration reads individual `words[].start`/`end` for word-by-word highlighting, adjust it to work at the chunk level instead, or use Sarvam's REST/Streaming endpoints where per-word timing isn't part of the response shape either.

## Diarization and long-form audio

Cartesia's `/stt` has no speaker-labeling mode to migrate from; this is new capability you get on Sarvam's side, through the Batch API:

```python
from cartesia import Cartesia

client = Cartesia(api_key="YOUR_CARTESIA_API_KEY")

# Not available: Cartesia's STT has no diarization or speaker-labeling mode.
# Multi-speaker audio transcribes as a single undifferentiated transcript.
transcription = client.stt.transcribe(
    file=open("meeting.mp3", "rb"),
    model="ink-whisper",
    language="en",
)
```

```python
from sarvamai import SarvamAI

client = SarvamAI(api_subscription_key="YOUR_SARVAM_API_KEY")

job = client.speech_to_text_job.create_job(
    model="saaras:v3",
    mode="transcribe",
    language_code="en-IN",
    with_diarization=True,
    num_speakers=3,
)

job.upload_files(file_paths=["meeting.mp3"])
job.start()
job.wait_until_complete()

file_results = job.get_file_results()
for f in file_results["failed"]:
    print(f"Failed: {f['file_name']}: {f['error_message']}")

if file_results["successful"]:
    job.download_outputs(output_dir="./output")
```

See the [Batch API guide](/api-reference-docs/api-guides-tutorials/speech-to-text/batch-api) for webhook setup and its [Speaker Diarization section](/api-reference-docs/api-guides-tutorials/speech-to-text/batch-api#speaker-diarization) for the current per-job speaker limit.

## Streaming

|                          | Cartesia                                                                                                                                                                                                                                    | Sarvam                                                                                                                                                                                                                                         |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Turn-detection WebSocket | `POST /stt/turns/websocket`, model `ink-2`, tuned with `turn_start_threshold`, `turn_eager_end_threshold`, `turn_end_threshold`, `turn_end_timeout_ms`; streams `turn.start`/`turn.update`/`turn.eager_end`/`turn.resume`/`turn.end` events | [WebSocket API](/api-reference-docs/api-guides-tutorials/speech-to-text/streaming-api) (`GET /speech-to-text/ws`) with `high_vad_sensitivity` and `vad_signals` enabled: one automatic "this turn just ended" signal, fewer thresholds to tune |
| Manual finalize          | `POST /stt/websocket`, model `ink-2` or `ink-whisper`; commit a segment with the plain-text message `finalize`                                                                                                                              | `flush_signal` plus an explicit `ws.flush()` call: force a transcript out on your own schedule instead of waiting for silence detection                                                                                                        |
| Connection-time config   | `model`, `language`, audio encoding/sample rate                                                                                                                                                                                             | `model`, `mode`, `language_code`, `sample_rate`, `input_audio_codec`                                                                                                                                                                           |

## What's different by design

* **Diarization comes standard on longer audio.** Cartesia's STT has no speaker-labeling mode; Sarvam's Batch API identifies and labels speakers automatically with `with_diarization` and `num_speakers`, no separate product or add-on needed.
* **Flexible output modes on one model.** `saaras:v3` supports `transcribe`, `translate`, `verbatim`, `translit`, and `codemix` modes on the same endpoint, rather than transcription being the only option.
* **One model, one workflow.** There's no dated version pin to track, and no separate model to switch to for real-time use: `saaras:v3` is the same model across REST, Batch, and Streaming.
* **Indian language depth is Sarvam's specific strength.** Saaras v3 covers 22 Indian languages, including Assamese, Konkani, Kashmiri, Sindhi, Sanskrit, Santali, Manipuri, Bodo, Maithili, and Dogri, with native handling for code-mixed speech like Hinglish.

## 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 transcribe(path: str) -> str:
    with open(path, "rb") as f:
        transcription = client.stt.transcribe(
            file=f,
            model="ink-whisper",
            language="en",
        )
    return transcription.text
```

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

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

async function transcribe(path) {
  const transcription = await client.stt.transcribe({
    file: fs.createReadStream(path),
    model: "ink-whisper",
    language: "en",
  });
  return transcription.text;
}
```

```bash
transcribe() {
  local path="$1"
  curl -sS -X POST https://api.cartesia.ai/stt \
    -H "Authorization: Bearer YOUR_CARTESIA_API_KEY" \
    -H "Cartesia-Version: 2025-04-16" \
    -F model="ink-whisper" \
    -F language="en" \
    -F file=@"$path"
}
```

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

client = SarvamAI(api_subscription_key="YOUR_SARVAM_API_KEY")

def transcribe(path: str, language_code: str = "en-IN") -> str:
    try:
        with open(path, "rb") as f:
            response = client.speech_to_text.transcribe(
                file=f,
                model="saaras:v3",
                language_code=language_code,
                mode="transcribe",
            )
    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
    return response.transcript
```

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

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

async function transcribe(path, languageCode = "en-IN") {
  try {
    const response = await client.speechToText.transcribe({
      file: fs.createReadStream(path),
      model: "saaras:v3",
      language_code: languageCode,
      mode: "transcribe",
    });
    return response.transcript;
  } 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;
  }
}
```

```bash
transcribe() {
  local path="$1" language_code="${2:-en-IN}"
  local body status
  body=$(curl -sS -w '\n%{http_code}' -X POST https://api.sarvam.ai/speech-to-text \
    -H "api-subscription-key: YOUR_SARVAM_API_KEY" \
    -F model="saaras:v3" \
    -F language_code="$language_code" \
    -F mode="transcribe" \
    -F file=@"$path")
  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.
  echo "${body%$'\n'*}" | grep -o '"transcript":"[^"]*"' | cut -d'"' -f4
}
```

## Common mistakes

It doesn't, so there's nothing to port over parameter-for-parameter. Add `with_diarization=True` and `num_speakers` directly on a Sarvam Batch job; this is new configuration, not a renamed existing one.

Sarvam's REST endpoint caps at 30 seconds, unlike Cartesia's `/stt`, which accepts audio of any length in one call. Route anything longer through the Batch API instead of expecting the REST endpoint to chunk it for you.

Batch API timestamps cover sentence or phrase chunks, not individual words, unlike Cartesia's optional `timestamp_granularities: ["word"]`. If your integration builds word-by-word highlighting from `words[].start`/`end`, adjust it to work at the chunk level instead.

Calling the Batch endpoints directly over HTTP (rather than through the SDK) requires nesting parameters under `job_parameters`, e.g. `{"job_parameters": {"model": "saaras:v3", ...}}`. A flat body modeled on Cartesia's request shape won't be picked up.

Cartesia's `/stt/turns/websocket` only accepts `ink-2`, not the `ink-whisper` model used for batch requests. Sarvam has one model name, `saaras:v3`, to pass everywhere, so there's no equivalent swap to remember on Sarvam's side.

## See also

* [Migration overview](/api-reference-docs/migrations/from-cartesia/overview): base URL, auth, and SDK client changes
* [Which Speech-to-Text API to Use](/api-reference-docs/api-guides-tutorials/speech-to-text/which-api-to-use)
* [Batch API](/api-reference-docs/api-guides-tutorials/speech-to-text/batch-api)
* [Streaming API](/api-reference-docs/api-guides-tutorials/speech-to-text/streaming-api)
* [Speaker Diarization](/api-reference-docs/api-guides-tutorials/speech-to-text/batch-api#speaker-diarization)
* [Errors & Troubleshooting](/api-reference-docs/errors-troubleshooting)