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

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

This guide covers migrating ElevenLabs 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_id` for `model`, upload the audio file directly instead of passing a `source_url`, and pick the right Sarvam API for the job: REST for short synchronous requests, Batch for long-form or multi-speaker audio, and WebSocket for real-time streaming.

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

```python
from elevenlabs import ElevenLabs

client = ElevenLabs(api_key="YOUR_ELEVENLABS_API_KEY")

with open("audio.mp3", "rb") as f:
    transcription = client.speech_to_text.convert(
        file=f,
        model_id="scribe_v2",
        language_code="eng",
        tag_audio_events=True,
    )

print(transcription.text)
```

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

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

const transcription = await client.speechToText.convert({
  file: fs.createReadStream("audio.mp3"),
  modelId: "scribe_v2",
  languageCode: "eng",
  tagAudioEvents: true,
});

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

```bash
curl -X POST https://api.elevenlabs.io/v1/speech-to-text \
  -H "xi-api-key: YOUR_ELEVENLABS_API_KEY" \
  -F model_id="scribe_v2" \
  -F language_code="eng" \
  -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
```

`with_diarization` and `num_speakers` are Batch API parameters. For a request like this one under 30 seconds, use the REST call above; for diarized or longer audio, use the Batch workflow below.

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.

|                                          | ElevenLabs                                     | Sarvam                                                                                                                                                 |
| ---------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Synchronous transcription                | `POST /v1/speech-to-text`                      | `POST /speech-to-text` (up to 30 seconds of audio per request)                                                                                         |
| Long-form or multi-speaker transcription | `POST /v1/speech-to-text` with `webhook: true` | Batch API: `POST /speech-to-text/job/v1` (create), upload, start, then poll or receive a webhook callback, up to 2 hours per file and 20 files per job |
| Real-time streaming transcription        | `wss://.../v1/speech-to-text/realtime`         | `GET /speech-to-text/ws`                                                                                                                               |

## Parameter concept map

| ElevenLabs                                                                                                                                 | Sarvam                                                                                                 | Note                                                                                                                                |
| ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
| `model_id` (`scribe_v1` or `scribe_v2` for REST and webhook requests; `scribe_v2_realtime` is a separate value used only on the WebSocket) | `model` (`saaras:v3`)                                                                                  | One current model across REST, Batch, and Streaming, rather than a different model\_id per endpoint                                 |
| `file` or `source_url` (fetch directly from a hosted URL)                                                                                  | `file` (upload the audio/video file directly)                                                          | Download the source once, then upload it: a single, predictable step for every request                                              |
| `language_code` (optional, ISO-639-1/3, e.g. `eng`)                                                                                        | `language_code` (optional, BCP-47, e.g. `ta-IN`; pass `unknown` to auto-detect)                        | Same idea; `unknown` gives you the same auto-detect behavior as leaving ElevenLabs' `language_code` unset                           |
| `diarize` (boolean)                                                                                                                        | `with_diarization` (boolean, Batch API)                                                                | Same concept; on Sarvam this is a Batch job parameter alongside `num_speakers`                                                      |
| `tag_audio_events` (boolean, tags laughter/applause, default `true`)                                                                       | Not applicable                                                                                         | Sarvam transcripts contain spoken words only, so there's nothing to toggle for non-speech event tags                                |
| `webhook` (boolean flag on the same endpoint)                                                                                              | `callback` (`url` + `auth_token`, set when creating a Batch job)                                       | Async processing lives in the Batch API rather than a flag on the sync endpoint; same idea, one dedicated workflow for longer audio |
| Response: `text` + `words[]` (word-level timing, speaker IDs)                                                                              | Response: `transcript` + `diarized_transcript.entries[]` (Batch, with `speaker_id` and segment timing) | Field names differ; see the timestamps note below for granularity                                                                   |

Sarvam's Batch API returns timestamps per chunk (sentence or phrase segment), which is precise enough for subtitle alignment and audio navigation. If your ElevenLabs integration reads individual `words[].start`/`end` for word-by-word highlighting, adjust it to work at the chunk level instead.

## Diarization and long-form audio

ElevenLabs' `/v1/speech-to-text` has no separate mode for this; Sarvam's Batch API handles both:

```python
from elevenlabs import ElevenLabs

client = ElevenLabs(api_key="YOUR_ELEVENLABS_API_KEY")

transcription = client.speech_to_text.convert(
    file=open("meeting.mp3", "rb"),
    model_id="scribe_v2",
    diarize=True,
    num_speakers=3,
    webhook=True,
)
```

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

|                      | ElevenLabs                                                                                                                    | Sarvam                                                                                                                                                          |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Connection config    | Realtime WebSocket (`wss://.../v1/speech-to-text/realtime`): `model_id: scribe_v2_realtime`, `language_code`, `audio_format`  | [WebSocket API](/api-reference-docs/api-guides-tutorials/speech-to-text/streaming-api) (`GET /speech-to-text/ws`): `model="saaras:v3"`, `mode`, `language_code` |
| Streamed events      | `partial_transcript` events, then `committed_transcript` (or `committed_transcript_with_timestamps`) once a segment finalizes | Transcript for your chosen `mode` directly, plus `START_SPEECH`/`END_SPEECH` events when `vad_signals=true`                                                     |
| Finalizing a segment | `commit` flag on the audio chunk, or VAD `commit_strategy`                                                                    | `flush_signal` plus an explicit `ws.flush()` call, or automatic end-of-speech detection via `high_vad_sensitivity`                                              |

## What's different by design

* **One current model, not a version matrix.** ElevenLabs' `scribe_v1`, `scribe_v2`, and `scribe_v2_realtime` become a single `saaras:v3`, used the same way across REST, Batch, and Streaming.
* **Direct file upload, no third-party fetch.** Sarvam transcribes the file you send it. Download the source once (from a URL, cloud storage, or elsewhere) and upload it directly, so every request starts from the same predictable input.
* **Diarization and long-form audio share one workflow.** The Batch API handles speaker diarization, files up to 2 hours, and multiple files per job in a single job-based flow, using the same `with_diarization`/`num_speakers` parameters whether the request is a short call or a large batch.
* **A consistent response shape.** Whether diarization is on or off, Sarvam always returns the same `transcript` (plus `diarized_transcript` when enabled), rather than switching structure based on a multichannel or output-style flag.
* **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 dedicated `verbatim`, `translit`, and `codemix` output modes for code-mixed speech like Hinglish.

## 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 transcribe(path: str) -> str:
    with open(path, "rb") as f:
        transcription = client.speech_to_text.convert(
            file=f,
            model_id="scribe_v2",
            language_code="eng",
            tag_audio_events=True,
        )
    return transcription.text
```

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

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

async function transcribe(path) {
  const transcription = await client.speechToText.convert({
    file: fs.createReadStream(path),
    modelId: "scribe_v2",
    languageCode: "eng",
    tagAudioEvents: true,
  });
  return transcription.text;
}
```

```bash
transcribe() {
  local path="$1"
  curl -sS -X POST https://api.elevenlabs.io/v1/speech-to-text \
    -H "xi-api-key: YOUR_ELEVENLABS_API_KEY" \
    -F model_id="scribe_v2" \
    -F language_code="eng" \
    -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

Sarvam's `file` parameter expects an open file object or byte stream, not a URL string. If your ElevenLabs code used `source_url` to pull audio from a hosted link, download it once and upload the local file instead.

`with_diarization` is a Batch API parameter, not something the synchronous REST endpoint accepts. If you need diarized output, send the request through the Batch workflow, even for a short file.

Batch API timestamps cover sentence or phrase chunks, not individual words. If your ElevenLabs integration builds word-by-word highlighting from `words[].start`/`end`, adjust it to highlight 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 ElevenLabs' request shape won't be picked up.

There's only one current-generation model, `saaras:v3`, across every endpoint. Older `saarika`/`saaras:v2.5` model names you'll see elsewhere in Sarvam's docs are legacy and being phased out, not a version choice to replicate.

## See also

* [Migration overview](/api-reference-docs/migrations/from-elevenlabs/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)