ElevenLabs Free code examples in C#, Python, Java and JavaScript
The reference for natural text to speech, voice cloning and dubbing.
WHAT THIS PAGE COVERS
Streaming audio to the browser instead of waiting for the whole file.
eleven_flash_v2_5
elevenlabs
Model ids change. If a sample returns a 404 on the model, that is the first thing to check against the official docs.
ELEVENLABS SHIPS NO OFFICIAL .NET SDK — Python and TypeScript only, per their own docs. So this is the REST API through HttpClient, which is the more durable sample anyway: it cannot be broken by a community wrapper going unmaintained.
# Nothing to install. System.Net.Http is in the framework.
dotnet new console -o ElevenLabsDemo
setx ELEVENLABS_API_KEY "your-key" # Windows, then open a new shell
export ELEVENLABS_API_KEY="your-key" # macOS / Linux
The header is xi-api-key, not Authorization — a Bearer token here returns 401 with no explanation. The voice id is part of the path.
using System.Text;
var apiKey = Environment.GetEnvironmentVariable("ELEVENLABS_API_KEY")
?? throw new InvalidOperationException("ELEVENLABS_API_KEY is not set.");
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("xi-api-key", apiKey);
const string VoiceId = "JBFqnCBsd6RMkjVDRZzb";
var body = """
{
"text": "The first move is what sets everything in motion.",
"model_id": "eleven_flash_v2_5"
}
""";
using var response = await http.PostAsync(
$"https://api.elevenlabs.io/v1/text-to-speech/{VoiceId}",
new StringContent(body, Encoding.UTF8, "application/json"));
response.EnsureSuccessStatusCode();
await File.WriteAllBytesAsync("speech.mp3", await response.Content.ReadAsByteArrayAsync());
ReadAsByteArrayAsync waits for the last byte. HttpCompletionOption.ResponseHeadersRead returns as soon as the headers land, so you can copy the body through while it is still arriving.
using var request = new HttpRequestMessage(
HttpMethod.Post,
$"https://api.elevenlabs.io/v1/text-to-speech/{VoiceId}")
{
Content = new StringContent(body, Encoding.UTF8, "application/json"),
};
// Without ResponseHeadersRead this line blocks until generation is done.
using var streamed = await http.SendAsync(
request, HttpCompletionOption.ResponseHeadersRead);
streamed.EnsureSuccessStatusCode();
await using var source = await streamed.Content.ReadAsStreamAsync();
await using var destination = File.Create("speech.mp3");
await source.CopyToAsync(destination);
Same trick, with the browser as the destination. The key stays on the server, which is the reason to proxy at all — a key shipped to the browser is a public key.
app.MapGet("/speak", async (string text, IHttpClientFactory factory, HttpContext context) =>
{
var client = factory.CreateClient("elevenlabs");
// JsonSerializer.Serialize quotes and escapes the user's text, so a
// quotation mark in it cannot break out of the JSON body.
var payload =
"{\"text\":" + System.Text.Json.JsonSerializer.Serialize(text) +
",\"model_id\":\"eleven_flash_v2_5\"}";
using var request = new HttpRequestMessage(
HttpMethod.Post,
"https://api.elevenlabs.io/v1/text-to-speech/JBFqnCBsd6RMkjVDRZzb")
{
Content = new StringContent(payload, Encoding.UTF8, "application/json"),
};
using var upstream = await client.SendAsync(
request, HttpCompletionOption.ResponseHeadersRead);
context.Response.ContentType = "audio/mpeg";
await (await upstream.Content.ReadAsStreamAsync()).CopyToAsync(context.Response.Body);
});
One package. The key goes in the environment — ElevenLabs bills per character, so a key in a repository is a bill in someone else's name.
pip install elevenlabs
export ELEVENLABS_API_KEY="your-key" # macOS / Linux
setx ELEVENLABS_API_KEY "your-key" # Windows, then open a new shell
convert() returns an ITERATOR of byte chunks, not a bytes object. Writing it straight to a file with f.write(audio) fails, and it is the first thing everyone hits.
import os
from elevenlabs.client import ElevenLabs
client = ElevenLabs(api_key=os.environ["ELEVENLABS_API_KEY"])
audio = client.text_to_speech.convert(
text="The first move is what sets everything in motion.",
voice_id="JBFqnCBsd6RMkjVDRZzb",
model_id="eleven_flash_v2_5",
output_format="mp3_44100_128",
)
with open("speech.mp3", "wb") as handle:
for chunk in audio:
handle.write(chunk)
stream() starts yielding before the sentence is finished. Measure time-to-first-byte rather than total time — total time barely moves, and the wait the user feels is the first byte.
import time
started = time.perf_counter()
first_chunk_at = None
total = 0
stream = client.text_to_speech.stream(
text="Streaming means playback starts before generation finishes.",
voice_id="JBFqnCBsd6RMkjVDRZzb",
model_id="eleven_flash_v2_5",
)
for chunk in stream:
if first_chunk_at is None:
first_chunk_at = time.perf_counter() - started
total += len(chunk)
print(f"first byte after {first_chunk_at:.2f}s, {total} bytes total")
Pass the iterator straight to the response — do not collect it into a list first, or you have paid for streaming and thrown away the benefit. This is the same shape FreeYantra's own TTS proxy uses.
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
app = FastAPI()
@app.get("/speak")
def speak(text: str):
stream = client.text_to_speech.stream(
text=text,
voice_id="JBFqnCBsd6RMkjVDRZzb",
model_id="eleven_flash_v2_5",
)
# StreamingResponse pulls from the iterator as the client reads,
# so the first audio leaves this server before ElevenLabs has
# finished generating the last of it.
return StreamingResponse(stream, media_type="audio/mpeg")
ELEVENLABS SHIPS NO OFFICIAL JAVA SDK — Python and TypeScript only. java.net.http has been in the JDK since 11, so there is no dependency to add and nothing to keep up to date.
# No dependency. java.net.http is part of the JDK (11+).
export ELEVENLABS_API_KEY="your-key"
The header is xi-api-key. BodyHandlers.ofFile writes straight to disk without holding the whole clip in memory, which matters once the text gets long.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Path;
public class Speak {
static final String VOICE_ID = "JBFqnCBsd6RMkjVDRZzb";
public static void main(String[] args) throws Exception {
String apiKey = System.getenv("ELEVENLABS_API_KEY");
if (apiKey == null) throw new IllegalStateException("ELEVENLABS_API_KEY is not set.");
String body = """
{
"text": "The first move is what sets everything in motion.",
"model_id": "eleven_flash_v2_5"
}
""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.elevenlabs.io/v1/text-to-speech/" + VOICE_ID))
.header("xi-api-key", apiKey)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpClient.newHttpClient().send(
request, HttpResponse.BodyHandlers.ofFile(Path.of("speech.mp3")));
}
}
ofInputStream() hands back the body as it arrives instead of after it finishes. Anything that needs to start playing before generation ends has to use this handler — swap the FileOutputStream below for a servlet's output stream and the audio reaches the browser while ElevenLabs is still speaking.
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
HttpResponse<InputStream> response = HttpClient.newHttpClient().send(
request, HttpResponse.BodyHandlers.ofInputStream());
if (response.statusCode() != 200) {
throw new IllegalStateException("ElevenLabs returned " + response.statusCode());
}
try (InputStream audio = response.body();
OutputStream sink = new FileOutputStream("speech.mp3")) {
audio.transferTo(sink);
}
ofInputStream gives you a stream whatever the status code, so a 401 or 429 arrives as an unread stream rather than a message. Read it, or the useful part of the failure is thrown away.
HttpResponse<InputStream> response = HttpClient.newHttpClient().send(
request, HttpResponse.BodyHandlers.ofInputStream());
if (response.statusCode() != 200) {
String detail;
try (InputStream error = response.body()) {
detail = new String(error.readAllBytes());
}
// 401 is a bad or revoked key and will never succeed.
// 429 is character quota — retrying doubles the spend.
throw new IllegalStateException(
"ElevenLabs " + response.statusCode() + " for model eleven_flash_v2_5: " + detail);
}
The package is @elevenlabs/elevenlabs-js. The old unscoped `elevenlabs` name on npm is a different, unmaintained thing — installing it is a confusing afternoon.
npm install @elevenlabs/elevenlabs-js
export ELEVENLABS_API_KEY="your-key"
The voice id is a positional first argument here, not a field in the options object — the one place the JS surface differs from Python's.
import { writeFile } from "node:fs/promises";
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
const client = new ElevenLabsClient({
apiKey: process.env.ELEVENLABS_API_KEY,
});
const audio = await client.textToSpeech.convert(
"JBFqnCBsd6RMkjVDRZzb",
{
text: "The first move is what sets everything in motion.",
modelId: "eleven_flash_v2_5",
outputFormat: "mp3_44100_128",
},
);
// A web ReadableStream, so collect the chunks before writing a file.
const chunks = [];
for await (const chunk of audio) chunks.push(chunk);
await writeFile("speech.mp3", Buffer.concat(chunks));
Piping the stream through means the first audio reaches the browser while ElevenLabs is still generating. Collecting it into a Buffer first — the obvious refactor — silently removes the whole benefit.
import express from "express";
import { Readable } from "node:stream";
const app = express();
app.get("/speak", async (request, response) => {
const stream = await client.textToSpeech.stream(
"JBFqnCBsd6RMkjVDRZzb",
{
text: String(request.query.text ?? "Hello from ElevenLabs."),
modelId: "eleven_flash_v2_5",
},
);
response.setHeader("Content-Type", "audio/mpeg");
Readable.fromWeb(stream).pipe(response);
});
app.listen(3000);
A 401 is a bad key and will never succeed. A 429 is quota and might — but retrying a text-to-speech call blindly doubles the character spend, so it belongs behind a deliberate decision rather than a generic retry wrapper.
async function speak(text) {
try {
return await client.textToSpeech.convert("JBFqnCBsd6RMkjVDRZzb", {
text,
modelId: "eleven_flash_v2_5",
});
} catch (error) {
// The SDK surfaces the HTTP status; 401 is fatal, 429 is quota.
if (error?.statusCode === 401) {
throw new Error("ELEVENLABS_API_KEY is missing or revoked.");
}
if (error?.statusCode === 429) {
throw new Error("Character quota exhausted — do not auto-retry.");
}
throw error;
}
}
QUESTIONS
Which languages are the ElevenLabs examples in?
C#, Python, Java and JavaScript. Every sample runs as pasted and reads its credentials from an environment variable — no key is ever written into the code.
Which package do I install for ElevenLabs?
The samples use elevenlabs. The install step on each language tab gives the exact command.
Which ElevenLabs model do these examples use?
eleven_flash_v2_5, checked against the official documentation on 26 September 2026. Model ids change often; if a call returns a model-not-found error, check the current list in the official docs.
Where is the official ElevenLabs documentation?
https://elevenlabs.io/docs — it is linked at the bottom of this page, and it is the authority whenever it disagrees with a sample here.
ALSO IN SPEECH & AUDIO
The authority
Official ElevenLabs documentation
Model names, pricing and rate limits change faster than any page here can. When this page and the official docs disagree, the official docs are right.
https://elevenlabs.io/docs