# Svara TTS Full Documentation > The complete Svara developer documentation as a single Markdown file, for LLMs. > Live docs: https://docs.kenpathlabs.com --- # Introduction Text to speech over HTTP and WebSocket, in 80 languages, from one endpoint. ## Overview The Svara API converts text to speech over HTTP and WebSocket. One model covers 80 languages and code-switches within a single request, with a library of 320 voices and eight output formats. Streamed, first audio typically arrives in a few hundred milliseconds. - [POST /v1/audio/speech](https://docs.kenpathlabs.com/text-to-speech.md) synthesizes text you already have, buffered or streamed - [WS /v1/audio/speech/stream-input](https://docs.kenpathlabs.com/input-streaming.md) takes text as it is produced — an LLM token stream, for example — and returns audio continuously - `ulaw` and `alaw` at 8 kHz come out of the API directly, so telephony needs no transcoding step ## Base URL ```text https://api.kenpathlabs.com/v1 ``` ## Compatibility The API is request-compatible with the OpenAI and ElevenLabs speech APIs. If your code already calls either one, change the base URL and the key; no other edits are required. - **OpenAI**: `POST /v1/audio/speech` — the primary endpoint, and the one that exposes every Svara parameter. The official OpenAI SDKs work unmodified. - **ElevenLabs**: `POST /v1/text-to-speech/{voice_id}` and the related streaming, timestamp, and voices endpoints. The official ElevenLabs SDKs work unmodified, including their realtime WebSocket client. - **Native WebSocket**: `/v1/audio/speech/stream-input` — Svara-specific, and the lowest-latency path for LLM-driven speech. Per-SDK setup, including the `/v1` difference between the two base URL conventions, is on [SDKs & compatibility](https://docs.kenpathlabs.com/sdks.md). ## Your first call ```http POST /v1/audio/speech ``` ```python # pip install git+https://github.com/kenpath-labs/svara-python.git from svara import Svara client = Svara() # reads SVARA_API_KEY audio = client.speech.create( input="नमस्ते! Welcome to Svara.", voice="sv_enhdbrj5", # Aanya response_format="mp3", ) open("hello.mp3", "wb").write(audio) ``` ```bash curl -X POST https://api.kenpathlabs.com/v1/audio/speech \ -H "Authorization: Bearer $SVARA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "voice": "sv_enhdbrj5", "input": "नमस्ते! Welcome to Svara.", "response_format": "mp3" }' --output hello.mp3 # voice sv_enhdbrj5 = Aanya ``` ```python from openai import OpenAI client = OpenAI(base_url="https://api.kenpathlabs.com/v1", api_key=SVARA_API_KEY) audio = client.audio.speech.create( model="svara-1", voice="sv_enhdbrj5", # Aanya input="नमस्ते! Welcome to Svara.", response_format="mp3", ) audio.write_to_file("hello.mp3") ``` ```javascript import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.kenpathlabs.com/v1", apiKey: SVARA_API_KEY }); const res = await client.audio.speech.create({ model: "svara-1", voice: "sv_enhdbrj5", // Aanya input: "नमस्ते! Welcome to Svara.", response_format: "mp3", }); await Bun.write("hello.mp3", await res.arrayBuffer()); ``` That mixed Hindi-English input is intentional: send raw text in any supported language, code-switching included, and the model handles it. No SSML, no phoneme markup. ## Next steps - [Quickstart](https://docs.kenpathlabs.com/quickstart.md): key, first request, and streaming in five minutes - [Text to speech](https://docs.kenpathlabs.com/text-to-speech.md): every request parameter explained - [Input streaming](https://docs.kenpathlabs.com/input-streaming.md): wire an LLM’s output straight into speech - [SDKs & compatibility](https://docs.kenpathlabs.com/sdks.md): using the OpenAI and ElevenLabs SDKs > A machine-readable OpenAPI spec is served live at `https://api.kenpathlabs.com/openapi.json`: handy for generating typed clients. --- # Quickstart Create a key, make a request, and stream the response. ## Get an API key Create a key in the [console under API keys](https://platform.kenpathlabs.com/dashboard/keys). Keys look like `sk_live_…` and are shown once at creation; store them in a secret manager or environment variable, never in client code or version control. ```bash export SVARA_API_KEY="sk_live_..." ``` ## Make your first request The `svara-voice` SDK is the shortest path. If you already use the OpenAI SDK, point it at the Svara base URL instead — the request shape is the same: ```bash pip install git+https://github.com/kenpath-labs/svara-python.git ``` ```python from svara import Svara client = Svara() # reads SVARA_API_KEY audio = client.speech.create( input="नमस्ते! Welcome to Svara.", voice="sv_enhdbrj5", # Aanya response_format="mp3", ) open("hello.mp3", "wb").write(audio) ``` ```python import os from openai import OpenAI client = OpenAI( base_url="https://api.kenpathlabs.com/v1", api_key=os.environ["SVARA_API_KEY"], ) audio = client.audio.speech.create( model="svara-1", voice="sv_enhdbrj5", # Aanya input="The quick brown fox jumps over the lazy dog.", response_format="mp3", ) audio.write_to_file("out.mp3") ``` ```javascript import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.kenpathlabs.com/v1", apiKey: process.env.SVARA_API_KEY, }); const res = await client.audio.speech.create({ model: "svara-1", voice: "sv_enhdbrj5", // Aanya input: "The quick brown fox jumps over the lazy dog.", response_format: "mp3", }); await Bun.write("out.mp3", await res.arrayBuffer()); ``` ```bash curl -X POST https://api.kenpathlabs.com/v1/audio/speech \ -H "Authorization: Bearer $SVARA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"voice": "sv_enhdbrj5", "input": "The quick brown fox jumps over the lazy dog.", "response_format": "mp3"}' \ --output out.mp3 # voice sv_enhdbrj5 = Aanya ``` ## Stream it Set `stream: true` and consume chunks as they arrive; with `pcm` output the first bytes land in a few hundred milliseconds rather than after the whole clip is generated. If the text comes from an LLM, use the [WebSocket input-streaming API](https://docs.kenpathlabs.com/input-streaming.md) instead — it accepts text while the model is still writing, so synthesis doesn’t wait for a complete sentence: ```python from svara import Svara client = Svara() for chunk in client.speech.stream( input="This sentence starts playing before it finishes generating.", voice="sv_enhdbrj5", # Aanya response_format="pcm", # 24 kHz, 16-bit LE, mono ): player.feed(chunk) # your audio sink # from an LLM? one WebSocket, speech before the sentence ends: # async for audio in AsyncSvara().speech.stream_input(deltas(), voice="sv_enhdbrj5"): ... ``` ```python with client.audio.speech.with_streaming_response.create( model="svara-1", voice="sv_enhdbrj5", # Aanya input="This sentence starts playing before it finishes generating.", response_format="pcm", # 24 kHz, 16-bit LE, mono extra_body={"stream": True}, ) as response: for chunk in response.iter_bytes(): player.feed(chunk) # your audio sink ``` ```bash curl -N -X POST https://api.kenpathlabs.com/v1/audio/speech \ -H "Authorization: Bearer $SVARA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"voice": "sv_enhdbrj5", "stream": true, "response_format": "pcm", "input": "This sentence starts playing before it finishes generating."}' \ | play -t raw -r 24000 -e signed -b 16 -c 1 - # sox; voice sv_enhdbrj5 = Aanya ``` Format choices, playback, and telephony output are on the [Streaming](https://docs.kenpathlabs.com/streaming.md) page. ## Explore voices and languages These endpoints are public, no key required: - `GET /v1/voices`: the current voice roster, each with a `preview_url` - `GET /v1/languages`: all 80 supported languages, for building pickers - `GET /v1/models`: available models > The [console playground](https://platform.kenpathlabs.com/dashboard/playground) runs the same API from the browser: try voices, languages, and multi-speaker scenes without writing code. --- # Authentication API key headers, key management, and authentication errors. ## Auth headers Every authenticated request carries your API key in a header. Both conventions are accepted everywhere, on every endpoint. Use whichever your client library already sends: ``` curl https://api.kenpathlabs.com/v1/audio/speech \ -H "Authorization: Bearer $SVARA_API_KEY" \ ... ``` ``` curl https://api.kenpathlabs.com/v1/audio/speech \ -H "xi-api-key: $SVARA_API_KEY" \ ... ``` WebSocket connections authenticate the same way: send the header on the upgrade request. Discovery endpoints (`/v1/voices`, `/v1/languages`, `/v1/models`) are public and need no key. > API keys are server-side credentials. Don’t embed them in web or mobile clients. Proxy TTS calls through your backend, or scope a low-limit key per environment. ## Managing keys Keys are created and revoked in the [console under API keys](https://platform.kenpathlabs.com/dashboard/keys). Each key: - is shown in full exactly once, at creation (we store only a hash) - carries its own plan: concurrency, requests-per-minute, and monthly character limits are per key (see [Rate limits](https://docs.kenpathlabs.com/rate-limits.md)) - reports its own usage; the [usage page](https://platform.kenpathlabs.com/dashboard/usage) breaks down requests, characters, and audio minutes per key - can be revoked instantly, without affecting your other keys A common pattern: one key per environment (`prod`, `staging`) or per integration, named accordingly, so usage attribution and revocation stay surgical. ## Auth errors Authentication failures return `401` with a structured body: ```json { "detail": { "status": "invalid_api_key", "message": "API key invalid or revoked." } } ``` | Parameter | Type | Default | Description | |---|---|---|---| | `missing_api_key` | 401 | - | No key found in either header. Send xi-api-key or Authorization: Bearer. | | `invalid_api_key` | 401 | - | The key doesn’t exist or has been revoked. | These shapes match what the official ElevenLabs SDKs expect, so their built-in error handling works unchanged. --- # Text to speech POST /v1/audio/speech: parameters, output formats, and language handling. ## Endpoint ```http POST /v1/audio/speech ``` JSON in, audio bytes out. One request shape covers plain synthesis, streaming, and every output format. The response body is the raw audio in your requested `response_format` (or a chunked stream when `stream: true`). > Feeding text from an LLM, or holding a conversation? Use the [WebSocket input-streaming API](https://docs.kenpathlabs.com/input-streaming.md) instead of per-request HTTP — one connection, speech that starts before the sentence ends. ## Request parameters Only `input` is required. Everything else has a serving-tuned default: **omit any parameter you don’t explicitly need** rather than hardcoding a value, so your integration inherits improvements automatically. | Parameter | Type | Default | Description | |---|---|---|---| | `input` | string | `required` | Text to synthesize (max 5,000 chars). Raw graphemes in any supported language; code-switching within one string is fine. No SSML or phonemes. | | `voice` | string | `-` | Library voice id from [GET /v1/voices](https://docs.kenpathlabs.com/voices.md): an `sv_`-prefixed id, e.g. `sv_enhdbrj5` (Aanya). | | `model` | string | `svara-1` | Optional. Any value is accepted (OpenAI-SDK compatibility); generation always uses the current Svara model. | | `response_format` | enum | `wav` | One of `mp3 opus aac flac wav pcm ulaw alaw`. See Output formats below. | | `sample_rate` | int | `24000` | Output rate in Hz: `8000 16000 22050 24000 32000 44100 48000`. Codec is 24 kHz native; others resampled in-process. | | `speed` | float | `1.0` | Speaking speed, `0.7`–`1.5`. Pitch is preserved — only the pace changes. Works on streaming responses too. | | `stream` | bool | `false` | Stream the audio as it’s generated. See [Streaming](https://docs.kenpathlabs.com/streaming.md). | | `lang` | string | `auto` | Language hint in any form (`hi`, `hin`, `hindi`, `hi-IN`). Enables number/unit normalization. Omit to auto-detect from script. | | `bitrate_kbps` | int | - | For lossy formats (mp3/opus/aac). Defaults: mp3 128, opus 64, aac 96. | | `temperature, top_p, top_k, min_p, repetition_penalty, presence_penalty` | float | - | Sampling knobs. Defaults are mode-aware and serving-tuned; leave them unset unless you have a measured reason. | > Using an OpenAI SDK? The extended fields (`stream`, `lang`, `sample_rate`, sampling knobs) ride in the SDK’s `extra_body` parameter; see [SDKs](https://docs.kenpathlabs.com/sdks.md). ## Output formats | Parameter | Type | Default | Description | |---|---|---|---| | `mp3` | lossy | - | Universal, small. Default 128 kbps. Best for web delivery and storage. | | `opus` | lossy | - | Efficient at low bitrates; great for real-time voice and WebRTC. | | `aac` | lossy | - | Apple-ecosystem friendly. | | `flac` | lossless | - | Archival / further processing without generation loss. | | `wav` | pcm container | - | Uncompressed, widely readable. The default. | | `pcm` | raw | - | Headerless 16-bit LE mono at your sample_rate. Lowest-latency streaming: feed straight to an audio sink. | | `ulaw / alaw` | telephony | - | 8-bit companded for telephony; pair with sample_rate 8000. | ## Languages & normalization The model covers 80 languages and switches between them mid-sentence. Send `lang` when you know it (any ISO form works) and the server normalizes numbers, dates, currency, and units into spoken form for that language. When you don’t know it, send nothing; the script is auto-detected. Don’t expose a “normalize” toggle in your UI; leave it on and let the server own it. Drive language pickers from `GET /v1/languages`. ## Examples ```bash curl -X POST https://api.kenpathlabs.com/v1/audio/speech \ -H "Authorization: Bearer $SVARA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "voice": "sv_r22w7pwe", "input": "Your appointment is confirmed for 3 PM tomorrow.", "response_format": "ulaw", "sample_rate": 8000 }' --output prompt.ulaw # voice sv_r22w7pwe = Aarav ``` ```bash curl -X POST https://api.kenpathlabs.com/v1/audio/speech \ -H "Authorization: Bearer $SVARA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "voice": "sv_enhdbrj5", "lang": "hi", "input": "आपका बिल ₹1,250 है और 15 अगस्त तक देय है।", "response_format": "mp3" }' --output bill.mp3 # voice sv_enhdbrj5 = Aanya ``` ```python audio = client.audio.speech.create( model="svara-1", voice="sv_r22w7pwe", # Aarav input="Your appointment is confirmed for 3 PM tomorrow.", response_format="mp3", extra_body={"lang": "en"}, ) audio.write_to_file("prompt.mp3") ``` --- # Streaming Receive audio over HTTP as it is generated, including telephony formats. > HTTP streaming is for text that already exists in full. If the text is still being produced — an LLM writing a reply, for example — use the [WebSocket input-streaming API](https://docs.kenpathlabs.com/input-streaming.md) instead: one connection per turn rather than one request per sentence, and synthesis starts before the sentence ends. It is also what the LiveKit and Pipecat integrations use. ## How it works Set `stream: true` on `POST /v1/audio/speech` and the server returns audio in a chunked HTTP response as it is generated, rather than buffering the whole clip first. Time to first byte stays flat as the text gets longer, so this is the default worth using for anything beyond a short phrase. ## Formats & latency `pcm` is the lowest-latency path: headerless 16-bit LE mono at your `sample_rate` (24 kHz default), playable the instant bytes arrive. Compressed formats (`mp3`, `opus`) stream too, but add a little container overhead. > Negotiate capabilities from `GET /health`: the `engine` and `stream_formats` fields tell you exactly what the current deployment streams. Don’t assume; read it. ## Native & server playback On native platforms, request `pcm` and write bytes to your audio sink as they arrive; first bytes land in a few hundred milliseconds on GPU serving. Re-align to 2-byte frames across chunk boundaries (a chunk can split a sample). ```python import pyaudio pa = pyaudio.PyAudio() out = pa.open(format=pyaudio.paInt16, channels=1, rate=24000, output=True) with client.audio.speech.with_streaming_response.create( model="svara-1", voice="sv_enhdbrj5", input=text, # voice: Aanya response_format="pcm", extra_body={"stream": True}, ) as response: tail = b"" for chunk in response.iter_bytes(): data = tail + chunk n = len(data) - (len(data) % 2) # whole samples only out.write(data[:n]); tail = data[n:] ``` ## Telephony For phone systems, request companded 8 kHz audio directly; no transcoding step in your media server: - `{ "response_format": "ulaw", "sample_rate": 8000 }`: North American / μ-law - `{ "response_format": "alaw", "sample_rate": 8000 }`: European / A-law Resampling runs in-process and scales: 8 kHz streams are cheap, so this fans out to many concurrent calls. --- # Input streaming Send text over a WebSocket as it is produced and receive audio continuously. ## When to use it Use this API when the text is still being produced — typically an LLM’s token stream. Rather than buffering output into sentences and synthesizing one at a time, the server consumes fragments as they arrive and holds back only a few words: the model generates each audio chunk from everything already spoken plus a short lookahead, so it never needs a complete sentence. - Speech starts about ten words into the stream, mid-sentence: 0.86 s to first audio versus 1.92 s for sentence buffering on the same simulated LLM stream - Against a live LLM at typical API cadence, audio began ~450 ms after the model’s first token - Prosody is continuous across chunk seams — each chunk is generated with the preceding audio as context, so there are no restarts or intonation resets at chunk boundaries - The connection is opened once, not per utterance, so each reply skips DNS, TLS, and admission. Measured from an Indian client against production: first audio ~420 ms after connection open, token feed included Two WebSocket endpoints expose this: a native one (recommended) and an ElevenLabs-compatible one for existing EL integrations. Both authenticate with your usual header on the upgrade request; see [Authentication](https://docs.kenpathlabs.com/authentication.md). ## Native WebSocket ```http WS /v1/audio/speech/stream-input ``` Connect with query parameters, send text fragments as JSON messages the moment they’re produced, and read binary frames back: raw PCM, 16-bit LE mono at 24 kHz. The server chunks at safe boundaries and keeps rolling text-and-audio context, so chunk N+1 continues chunk N’s prosody. Don’t pre-chunk or sentence-split yourself. | Parameter | Type | Default | Description | |---|---|---|---| | `voice` | query | `-` | Voice id from GET /v1/voices: an sv_-prefixed id, e.g. sv_enhdbrj5 (Aanya). | | `lang` | query | - | Language hint: any form works (`hi`, `hin`, `hindi`, `hi-IN`). Omit to auto-detect from script. | | `mode` | query | `sentence` | Chunking strategy: `sentence` (waits for sentence boundaries) or `eager` (word-level, lowest latency; see below). | | `speed` | query | `1.0` | Speaking speed, `0.7`–`1.5`, pitch-preserving. Applied continuously across the whole session, so chunk seams stay seamless. | | `temperature …` | query | - | Sampling knobs ride as query params too. Omit them; the server fills serving-tuned defaults. | ### Messages you send | Parameter | Type | Default | Description | |---|---|---|---| | `{"text": "fragment "}` | JSON | - | A text fragment, as small as a single token delta. Send them as fast as they arrive. | | `{"flush": true}` | JSON | - | Force everything buffered out as audio now (for example at the end of an LLM paragraph). | | `{"text": ""}` | JSON | - | End of stream. The server synthesizes the remainder, sends `{"type": "done"}`, and closes. | ```bash pip install git+https://github.com/kenpath-labs/svara-python.git ``` ```python import asyncio from openai import AsyncOpenAI from svara import AsyncSvara llm = AsyncOpenAI() svara = AsyncSvara() # reads SVARA_API_KEY async def speak(prompt: str): async def deltas(): stream = await llm.chat.completions.create( model="gpt-4o-mini", stream=True, messages=[{"role": "user", "content": prompt}]) async for event in stream: if event.choices[0].delta.content: yield event.choices[0].delta.content # One WebSocket; audio starts a few words into the LLM's output. async for audio in svara.speech.stream_input(deltas(), voice="sv_enhdbrj5"): player.feed(audio) # PCM16 @ 24 kHz mono asyncio.run(speak("Explain how rainbows form, in three sentences.")) ``` ```python import asyncio, json, os, websockets from openai import AsyncOpenAI llm = AsyncOpenAI() URL = "wss://api.kenpathlabs.com/v1/audio/speech/stream-input?voice=sv_enhdbrj5&mode=eager" # voice: Aanya async def speak(prompt: str): headers = {"Authorization": f"Bearer {os.environ['SVARA_API_KEY']}"} async with websockets.connect(URL, additional_headers=headers) as ws: async def feed(): stream = await llm.chat.completions.create( model="gpt-4o-mini", stream=True, messages=[{"role": "user", "content": prompt}]) async for event in stream: delta = event.choices[0].delta.content or "" if delta: await ws.send(json.dumps({"text": delta})) await ws.send(json.dumps({"text": ""})) # end of stream feeder = asyncio.create_task(feed()) async for frame in ws: # binary = PCM16 @ 24 kHz if isinstance(frame, bytes): player.feed(frame) # your audio sink elif json.loads(frame).get("type") == "done": break await feeder asyncio.run(speak("Explain how rainbows form, in three sentences.")) ``` ```javascript import WebSocket from "ws"; const ws = new WebSocket( "wss://api.kenpathlabs.com/v1/audio/speech/stream-input?voice=sv_enhdbrj5&mode=eager", // voice: Aanya { headers: { Authorization: `Bearer ${process.env.SVARA_API_KEY}` } }, ); ws.on("open", async () => { for await (const delta of llmTokenStream()) { // your LLM stream ws.send(JSON.stringify({ text: delta })); } ws.send(JSON.stringify({ text: "" })); // end of stream }); ws.on("message", (data, isBinary) => { if (isBinary) player.feed(data); // PCM16 @ 24 kHz mono else if (JSON.parse(data).type === "done") ws.close(); }); ``` ## Eager mode `mode=eager` is the lowest-latency configuration and maps directly onto how the model was trained. The server buffers incoming deltas at word granularity (mid-word token deltas are handled), and as soon as `chunk_words + peek_words` complete words exist it synthesizes the first `chunk_words` with the next few words attached as the lookahead peek: | Parameter | Type | Default | Description | |---|---|---|---| | `chunk_words` | query | `4` | Words per synthesis chunk. Smaller = earlier first audio, more chunk seams (they’re seamless, but each chunk has scheduling overhead). | | `peek_words` | query | `2` | Lookahead words held back so each chunk knows what’s coming (clamped 1–5). The final chunk goes out with no peek, which is the model’s end-of-utterance signal. | | `max_chunk_words` | query | `20` | Upper bound on a chunk once text has queued up — keeps a fast LLM from producing one enormous chunk. | With defaults, speech starts roughly 6 words into the LLM’s output; typically well before the first sentence ends. ## ElevenLabs-compatible WebSocket ```http WS /v1/text-to-speech/{voice_id}/stream-input ``` A drop-in implementation of the ElevenLabs realtime protocol, for existing EL integrations: the official EL SDKs’ WebSocket clients work unmodified. The full protocol is supported: - **BOS**: `{"text": " ", "generation_config": {"chunk_length_schedule": [120,160,250,290]}}` (schedule values clamped 50-500; `auto_mode` supported) - **Text**: `{"text": "fragment ", "try_trigger_generation": true}`, plus `{"flush": true}`; a bare space is a keepalive - **EOS**: `{"text": ""}` - **Audio frames**: `{"audio": "", "alignment": …, "normalizedAlignment": …}` in the negotiated `output_format` (query param, e.g. `pcm_24000`, `mp3_44100_128`), then `{"isFinal": true}` and a clean close (code 1000) - `inactivity_timeout` query param: 20 s default, 180 s max Character timings in `alignment` are chunk-relative and approximate: chunk boundaries are sample-exact, characters uniform within a chunk. ## Tips - **Forward raw deltas.** Don’t sentence-split, batch, or “clean up” the LLM stream client-side; the server’s chunker is the one that knows the model’s trained format. - **Buffer ~150 ms on playback.** A small jitter buffer on your audio sink absorbs network variance without hurting perceived latency. - **Keep the socket warm for turns, not sessions.** Open the connection when a reply starts, close on `{"type": "done"}`. Each connection is one utterance with one prosodic arc. - **Concurrent conversations need concurrent streams**: each open socket counts against your plan’s concurrency limit (see [Rate limits](https://docs.kenpathlabs.com/rate-limits.md)). --- # Pronunciation dictionaries Respelling rules applied to your text before synthesis. ## How it works A pronunciation dictionary is a list of per-organisation respelling rules applied to your text before synthesis. Create one in the console (Pronunciation), collect its id, and pass that id with any speech request — every rule in the dictionary applies to that request. ```http POST /v1/audio/speech ``` ``` { "voice": "sv_enhdbrj5", "input": "Your SQL dashboard and HDFC statement are ready.", "pronunciation_dictionary_id": "fbdb2572-a0ed-4bf0-b83a-897411f95090" } ``` ## Rules are respellings, not phonetics The model reads letters, so rules are written the way the word should be read — `SQL` as `sequel` (or `S Q L` to spell it out), `NASA` as `नासा`. IPA and phoneme notation are not accepted; a rule pasted as `/ˈnæsə/` is refused with an explanation of what to write instead. > Respellings can cross scripts: an English acronym can be respelled in Devanagari, and rules still match inside code-mixed sentences. ## Match options & language scope - `case_sensitive` — match only with exact casing (default off: `nasa` matches `NASA`). - `word_boundaries` — match whole words only (default on), Devanagari-aware. - `only_languages` / `except_languages` — scope a rule to (or away from) specific request languages. One term may carry different rules at different scopes: HDFC read as `एच डी एफ सी` in Hindi and as `H D F C` everywhere else. ## Using a dictionary in a request Pass `pronunciation_dictionary_id` on [speech requests](https://docs.kenpathlabs.com/text-to-speech.md) or as a query parameter on the [input-streaming WebSocket](https://docs.kenpathlabs.com/input-streaming.md). Rules apply before normalization, and edits propagate to running infrastructure within seconds — no redeploys. --- # Voices & languages The voice roster, preview clips, and the languages endpoint. ## The voice roster The library ships 320 human-reviewed voices across 78 native languages — every voice speaks all 80 supported languages; its native one is where its accent lives. Fetch the roster at runtime rather than hardcoding a list; store each voice’s `voice_id` and display its `name`. ```http GET /v1/voices ``` ```json { "voices": [ { "voice_id": "sv_enhdbrj5", "name": "Aanya", "category": "premade", "description": "The first voice Svara learned to love. Warm Hindi storytelling with a patient, late-evening calm.", "labels": { "native_language": "Hindi", "native_language_code": "hi", "accent": "indian", "quality_band": "A", "is_native_source": "true", "preview_language": "Hindi" }, "preview_url": "/v1/voices/sv_enhdbrj5/preview" }, ... ] } ``` - `voice_id` is a stable `sv_`-prefixed id; it is the only way to address a voice, on both the OpenAI-style and ElevenLabs-style endpoints - `labels.native_language` is where the voice’s accent comes from; it still speaks every supported language - names change, ids don’t — store the `voice_id` and render `name` from the response > The [console Voices page](https://platform.kenpathlabs.com/dashboard/voices) plays every voice and filters by language and gender, if you want to pick one before writing code. ## Voice previews Every voice has a bundled preview clip; use it instead of live-synthesizing sample audio (it’s instant and free): ```http GET /v1/voices/{voice_id}/preview ``` The `preview_url` is included in each voice object from `/v1/voices`, so you rarely construct this URL by hand. ## Languages The model handles 80 languages and code-switches between them within a single request. Drive language pickers from the languages endpoint: ```http GET /v1/languages ``` When calling TTS, the `lang` hint accepts any ISO form: `hi`, `hin`, `hindi`, `hi-IN` all resolve. Sending it enables number and unit normalization; omit it to auto-detect from the script. See [Text to speech](https://docs.kenpathlabs.com/text-to-speech.md) for details. --- # LiveKit Agents Use Svara as the TTS service in a LiveKit Agents pipeline. ## Overview [LiveKit Agents](https://docs.livekit.io/agents/) builds voice agents out of three swappable legs — STT, LLM, TTS — plus VAD and end-of-turn detection. Svara plugs into the TTS leg through the official Python SDK: `from svara.livekit import TTS`. By default it rides the [input-streaming WebSocket](https://docs.kenpathlabs.com/input-streaming.md), so the agent starts speaking while the LLM is still writing its sentence. ``` mic ─▶ VAD ─▶ STT ─▶ LLM ─┬▶ Svara TTS (eager WS) ─▶ speaker └── token stream, not sentences ``` The plugin emits **24 kHz, 16-bit, mono PCM** straight into LiveKit’s `AudioEmitter` — no decode, no resample on the hot path — and pins the platform-certified sampling defaults so the agent sounds exactly like the API it fronts. ## The TTS plugin Two synthesis paths, chosen by `mode`: - `mode="eager"` (default) — forwards the LLM token stream into the input-streaming WebSocket; speech starts a few words in and prosody stays continuous across the whole turn. Measured roughly **2× faster to first audio** than the sentence-buffered path in the same pipeline. - `mode="http"` — buffers whole sentences and synthesizes each over streaming HTTP. Used automatically for fixed-text `say()`, and available as the conservative fallback for full turns. ```bash pip install "svara-voice[livekit] @ git+https://github.com/kenpath-labs/svara-python.git" ``` ```python from svara.livekit import TTS tts = TTS(voice="sv_enhdbrj5") # Aanya; eager WS mode by default # tts = TTS(voice="sv_enhdbrj5", mode="http") # sentence-buffered fallback # tts = TTS(voice="sv_enhdbrj5", language="hi", pronunciation_dictionary_id="pd_...") ``` > The plugin’s source is small and readable — [svara/livekit/tts.py](https://github.com/kenpath-labs/svara-python/blob/main/src/svara/livekit/tts.py) in the SDK repo — if you need to fork its behavior rather than configure it. ## Wiring the agent Pass the plugin as `tts=` on the `AgentSession`. Every other leg is a stock plugin, and each is independently swappable — nothing below is Svara-specific. ```bash pip install 'livekit-agents[openai,silero,turn-detector]' ``` ```python from livekit import agents from livekit.agents import Agent, AgentServer, AgentSession, JobContext, JobProcess from livekit.plugins import openai, silero from svara.livekit import TTS server = AgentServer(setup_fnc=lambda proc: proc.userdata.__setitem__( "vad", silero.VAD.load())) @server.rtc_session(agent_name="svara-agent") async def entrypoint(ctx: JobContext) -> None: await ctx.connect() session = AgentSession( vad=ctx.proc.userdata["vad"], stt=openai.STT(model="gpt-4o-mini-transcribe"), llm=openai.LLM(model="gpt-4.1-mini"), tts=TTS(voice="sv_enhdbrj5"), # any id from GET /v1/voices ) await session.start( Agent(instructions="You are a helpful voice assistant. " "Reply in one or two short sentences."), room=ctx.room, ) await session.generate_reply(instructions="Greet the caller in one sentence.") if __name__ == "__main__": agents.cli.run_app(server) ``` | Parameter | Type | Default | Description | |---|---|---|---| | `SVARA_API_KEY` | env | - | Your Svara key (the SDK reads it automatically). | | `SVARA_BASE_URL` | env | `https://api.kenpathlabs.com` | The gateway origin; override for a regional pin (api-in, api-us, api-eu). | | `LIVEKIT_URL / _API_KEY / _API_SECRET` | env | - | Your LiveKit server or Cloud project. | Run `python agent.py download-files` once (it fetches the VAD and turn-detector models), then `python agent.py dev`. ## Streaming from the LLM With `mode="eager"` the plugin declares `streaming=True` and LiveKit hands it the raw token stream; the plugin forwards each delta over one WebSocket and pushes the binary PCM frames that come back. Nothing to implement — the protocol details in [Input streaming](https://docs.kenpathlabs.com/input-streaming.md) are what the plugin speaks for you. Prefer sentence-at-a-time synthesis for a specific agent? Construct the plugin with `mode="http"`, or flip it at runtime with `tts.update_options(mode="http")`. ## Gotchas - **Use the gateway host.** `https://api.kenpathlabs.com` serves the API; the console host does not. - **Don’t resample.** The plugin declares `sample_rate=24000` and LiveKit’s emitter takes the raw PCM; LiveKit itself resamples for an 8 kHz telephony leg. Anything you add on top only brings latency and artifacts. - **429s are already mapped.** The plugin converts Svara’s ElevenLabs-shaped errors into LiveKit’s `APIStatusError` with the right retryability, so the agent backs off on `rate_limit_exceeded` and `too_many_concurrent_requests` instead of dropping the turn. `insufficient_quota` is terminal until your quota resets. - **Use `sv_` voice ids in code.** Ids like `sv_enhdbrj5` come from [GET /v1/voices](https://docs.kenpathlabs.com/voices.md) and are stable across renames. - **Leave the sampling knobs alone.** The plugin pins the platform-certified sampling; overriding it per-agent is how long utterances start to ramble. --- # Pipecat Use Svara as the TTS service in a Pipecat pipeline. ## Overview [Pipecat](https://docs.pipecat.ai) builds voice agents as a pipeline of frame processors. Svara plugs in as the TTS service through the official Python SDK: `from svara.pipecat import SvaraTTSService`. Everything else — transport, STT, LLM, VAD, turn-taking — is stock. ``` transport.input() ─▶ STT ─▶ user aggregator ─▶ LLM │ Svara TTS ◀────────────┘ └─▶ transport.output() ─▶ assistant aggregator ``` The service streams **24 kHz, 16-bit, mono PCM** by default. Set the transport’s `audio_out_sample_rate` to match and nothing resamples on the hot path; for telephony transports construct it with `response_format="ulaw", sample_rate=8000` and the G.711 bytes drop straight onto the call leg. ## The TTS service Pipecat’s base class aggregates the LLM output into sentences and calls the service once per sentence; each sentence streams back over HTTP as it synthesizes, with TTFB metrics wired in. ```bash pip install "svara-voice[pipecat] @ git+https://github.com/kenpath-labs/svara-python.git" ``` ```python from svara.pipecat import SvaraTTSService tts = SvaraTTSService(voice="sv_enhdbrj5") # Aanya; reads SVARA_API_KEY # telephony: 8 kHz G.711 µ-law, no resampling anywhere # tts = SvaraTTSService(voice="sv_enhdbrj5", response_format="ulaw", sample_rate=8000) ``` > The integration is beta — validate against your pinned `pipecat-ai` version. The service’s source is one small file ([svara/pipecat/__init__.py](https://github.com/kenpath-labs/svara-python/blob/main/src/svara/pipecat/__init__.py)) if you need to adapt it. ## Wiring the pipeline Put the service where the TTS goes. Nothing else here is Svara-specific; swap STT or LLM for any other Pipecat service. ```bash pip install 'pipecat-ai[openai,silero,webrtc]' pipecat-ai-prebuilt ``` ```python import os from pipecat.audio.vad.silero import SileroVADAnalyzer from pipecat.frames.frames import LLMRunFrame from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.worker import PipelineWorker from pipecat.processors.aggregators.llm_context import LLMContext from pipecat.processors.aggregators.llm_response_universal import ( LLMContextAggregatorPair, LLMUserAggregatorParams, ) from pipecat.runner.types import RunnerArguments from pipecat.runner.utils import create_transport from pipecat.services.openai.llm import OpenAILLMService from pipecat.services.openai.stt import OpenAISTTService from pipecat.transports.base_transport import TransportParams from pipecat.workers.runner import WorkerRunner from svara.pipecat import SvaraTTSService SAMPLE_RATE = 24000 async def bot(runner_args: RunnerArguments) -> None: transport = await create_transport(runner_args, { "webrtc": lambda: TransportParams( audio_in_enabled=True, audio_out_enabled=True, audio_out_sample_rate=SAMPLE_RATE, # match Svara; no resampling ), }) context = LLMContext([{ "role": "system", "content": "You are a helpful voice assistant. " "Reply in one or two short sentences.", }]) aggregators = LLMContextAggregatorPair( context, # In Pipecat 1.6 VAD and turn-taking live on the user aggregator, # NOT on the transport. user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()), ) task = PipelineWorker(Pipeline([ transport.input(), OpenAISTTService(api_key=os.environ["OPENAI_API_KEY"]), aggregators.user(), OpenAILLMService(api_key=os.environ["OPENAI_API_KEY"]), SvaraTTSService(voice="sv_enhdbrj5"), # any id from GET /v1/voices transport.output(), aggregators.assistant(), ])) @transport.event_handler("on_client_connected") async def _(transport, client): context.add_message({"role": "system", "content": "Greet the caller in one sentence."}) await task.queue_frames([LLMRunFrame()]) await WorkerRunner(handle_sigint=False).run(task) if __name__ == "__main__": from pipecat.runner.run import main main() ``` | Parameter | Type | Default | Description | |---|---|---|---| | `SVARA_API_KEY` | env | - | Your Svara key (the SDK reads it automatically). | | `SVARA_BASE_URL` | env | `https://api.kenpathlabs.com` | The gateway origin; override for a regional pin (api-in, api-us, api-eu). | | `OPENAI_API_KEY` | env | - | For the stock STT and LLM services. | `python bot.py` starts the dev runner and serves a prebuilt WebRTC client at `/client` — no media account needed. ## Token streaming The service above receives whole sentences — Pipecat’s default text aggregation. To speak while the LLM is still writing, the underlying SDK client exposes the [input-streaming WebSocket](https://docs.kenpathlabs.com/input-streaming.md) directly: `client.speech.stream_input(token_stream, …)` takes any async iterator of text deltas and yields PCM as the model speaks. Pair it with `TextAggregationMode.TOKEN` and `append_to_audio_context()` in a custom processor; a first-class eager mode for `SvaraTTSService` is on the SDK roadmap. ## Gotchas - **Use the gateway host.** `https://api.kenpathlabs.com` serves the API; the console host does not. - **VAD is not a transport setting** in Pipecat 1.6. It moved to `LLMUserAggregatorParams(vad_analyzer=…)`, along with `user_turn_strategies`. Passing it to `TransportParams` fails validation. - **`PipelineTask` and `PipelineRunner` are deprecated** in favour of `PipelineWorker` and `WorkerRunner`. They are drop-in. - **The prebuilt UI package is `pipecat-ai-prebuilt`**. Install the wrong one and the server starts, prints “Bot ready”, and serves 404 at `/client`. - **Errors surface as `ErrorFrame`s** with Svara’s status inside (`rate_limit_exceeded`, `too_many_concurrent_requests`, `insufficient_quota`) — three different situations that call for three different responses. - **Leave the sampling knobs alone.** The server fills serving-tuned defaults; per-bot overrides are how long utterances start to ramble. --- # Rate limits & errors Plan limits, 429 handling, and the error status values. ## Plan limits Limits attach to your **organization**, not to individual API keys — every key in a workspace draws from the same pool, so creating more keys never raises a limit. There are three axes: concurrent streams, requests per minute, and characters per calendar month. Your workspace’s exact limits are shown in the [console settings](https://platform.kenpathlabs.com/dashboard/settings). | Limit | Free workspace | |---|---| | Concurrent WebSocket streams | 1 | | Concurrent HTTP streams | 2 | | Requests / minute | 10 | | Characters / month | 10,000 | > For higher limits, email [hello@kenpathlabs.com](mailto:hello@kenpathlabs.com) with your expected concurrency and monthly character volume. WebSocket and HTTP allowances are set independently, so a workload that is mostly one channel can be sized that way. - **Concurrent streams**: in-flight requests at once, across every key in the workspace. Each open connection (including a WebSocket) holds one slot; slots free on completion, and a crashed connection’s slot self-frees on a short TTL. - **Requests per minute**: fixed 60-second window, counted per organization. - **Characters per month**: summed `input` length over the calendar month (UTC), per organization. - **WebSocket vs HTTP**: the two channels are counted separately, and a plan can cap each on its own — on the free tier one long-lived WebSocket and two HTTP streams can be in flight at once. The console shows your workspace’s exact per-channel numbers. ## Handling 429s When a limit is hit you get `429` with a machine-readable `status` and, where applicable, a `Retry-After` header (seconds). Successful responses also carry your remaining budget: | Parameter | Type | Default | Description | |---|---|---|---| | `x-ratelimit-remaining-requests` | header | - | Requests left in the current minute window. | | `x-ratelimit-remaining-streams` | header | - | Concurrency slots free for your workspace right now. | | `x-ratelimit-remaining-characters` | header | - | Characters left this month (absent on unlimited plans). | Back off and retry on `rate_limit_exceeded` and `too_many_concurrent_requests` (retry after ~1 s for concurrency). Treat `insufficient_quota` as terminal until the month rolls over or your plan changes; don’t retry-storm it. ## Error catalogue All errors share one shape, so a single handler covers them: `{ "detail": { "status": "...", "message": "..." } }`. ``` { "detail": { "status": "rate_limit_exceeded", "message": "Request rate limit exceeded." } } ``` | Parameter | Type | Default | Description | |---|---|---|---| | `rate_limit_exceeded` | 429 | - | Too many requests this minute. Respect Retry-After. | | `too_many_concurrent_requests` | 429 | - | All concurrency slots for your workspace are in use. Retry in ~1 s. | | `insufficient_quota` | 429 | - | Monthly character quota exhausted. Terminal until reset/upgrade. | | `missing_api_key / invalid_api_key` | 401 | - | Auth problem; see [Authentication](https://docs.kenpathlabs.com/authentication.md). | > These `status` values intentionally match the OpenAI and ElevenLabs error vocabularies, so those SDKs’ built-in retry logic works against Svara unchanged. --- # Usage & subscription Read your plan, balance and month-to-date usage over the API. ## GET /v1/usage The numbers behind the meter, for the key you present: your plan and its limits, the live subscription window, this month's character count, and your remaining balance with its next expiry. It is the API version of the console's balance view — build your own gauge or alerting from it instead of scraping `x-ratelimit-*` response headers. ```http GET /v1/usage ``` Auth is your normal API key, either header convention (see [Authentication](https://docs.kenpathlabs.com/authentication.md)). Limits attach to your organization, so every key in the workspace reads the same numbers. ```bash curl https://api.kenpathlabs.com/v1/usage \ -H "xi-api-key: $SVARA_API_KEY" ``` ```python import os, requests usage = requests.get( "https://api.kenpathlabs.com/v1/usage", headers={"xi-api-key": os.environ["SVARA_API_KEY"]}, ).json() print(usage["balance"]["characters_remaining"], "characters left") ``` ```json { "organisation_id": "9b805cde-4c1a-4b8e-9f0e-…", "mode": "lots", "plan": { "id": "growth", "name": "Growth", "monthly_characters": 1000000, "requests_per_minute": 200, "max_concurrent_streams": 10 }, "subscription": { "plan_id": "growth", "expires_at": "2026-09-18T09:14:00+00:00", "auto_renew": true, "renewal_plan_id": null, "falls_back_to": "free" }, "month": { "period": "2026-08", "characters_used": 41230, "resets_at": "2026-09-01T00:00:00+00:00" }, "balance": { "characters_remaining": 958770, "total_granted": 1000000, "next_expiry": { "at": "2026-10-17T09:14:00+00:00", "amount": 958770 }, "lots": [ { "kind": "plan_grant", "remaining": 958770, "expires_at": "2026-10-17T09:14:00+00:00" } ] } } ``` ## Response fields | Parameter | Type | Default | Description | |---|---|---|---| | `mode` | string | - | How your balance is accounted: `lots` (prepaid character lots — the normal state), `wallet` or `allowance` (legacy accounting; the fields below degrade gracefully). | | `plan` | object | - | Your tier and the limits admission enforces: monthly characters, requests per minute, concurrent streams. -1 means unlimited. | | `subscription` | object | null | - | The live window: which plan, when it ends, whether autopay renews it, and `falls_back_to` — the plan you revert to if it lapses. | | `month.characters_used` | int | - | Characters metered this calendar month (UTC), across every key in the workspace. Only successful requests count. | | `month.resets_at` | timestamp | - | First instant of next month, UTC — when the monthly counter starts over. | | `balance.characters_remaining` | int | - | What you can spend right now. This is the same number admission checks. | | `balance.lots` | array | - | Your balance by cohort — plan grants, top-ups (`purchase`), promos — each with its own expiry. Whatever expires soonest is spent first. | | `balance.next_expiry` | object | null | - | The soonest cohort death: when, and how many characters go with it. | > Poll politely: these numbers move only when you synthesize, so once a minute is plenty for a dashboard. The endpoint is not rate-limit exempt — a tight loop spends your requests-per-minute budget. ## ElevenLabs dialect If you are using the ElevenLabs SDK against Svara (see [SDKs & compatibility](https://docs.kenpathlabs.com/sdks.md)), its subscription call works unchanged: ```http GET /v1/user/subscription ``` ```json { "tier": "growth", "status": "active", "character_count": 41230, "character_limit": 1000000, "can_extend_character_limit": false, "allowed_to_extend_character_limit": false, "next_character_count_reset_unix": 1760691240, "billing_period": "monthly_period", "character_refresh_period": "monthly_period", "currency": "inr" } ``` `character_count` and `character_limit` are cycle-shaped the way that SDK expects: consumed versus granted across your live cohorts, with `next_character_count_reset_unix` set to the soonest expiry. For the richer per-cohort picture, prefer `/v1/usage`. --- # SDKs & compatibility The Svara Python SDK, and using the OpenAI or ElevenLabs SDKs against Svara. ## Svara Python SDK The official SDK. One dependency-light package (`httpx` + `websockets`) that covers the whole surface: sync and async clients, HTTP streaming, the [input-streaming WebSocket](https://docs.kenpathlabs.com/input-streaming.md) behind one method call, pronunciation dictionaries, a CLI, and drop-in [LiveKit](https://docs.kenpathlabs.com/livekit.md) / [Pipecat](https://docs.kenpathlabs.com/pipecat.md) plugins. The distribution is `svara-voice`; the import is `import svara`. ```bash pip install git+https://github.com/kenpath-labs/svara-python.git ``` ```python from svara import Svara client = Svara() # reads SVARA_API_KEY audio = client.speech.create( input="नमस्ते! Welcome to Svara.", voice="sv_enhdbrj5", # Aanya response_format="mp3", ) open("hello.mp3", "wb").write(audio) # low latency: stream chunks as they generate for chunk in client.speech.stream(input="...", voice="sv_enhdbrj5", response_format="pcm"): player.feed(chunk) ``` ```python from svara import AsyncSvara client = AsyncSvara() # the endpoint voice agents should be on: feed an LLM token stream, # audio starts before the sentence ends (one WebSocket, not N requests) async for audio in client.speech.stream_input(llm_deltas(), voice="sv_enhdbrj5"): player.feed(audio) ``` ``` svara say "नमस्ते दुनिया" --voice sv_enhdbrj5 --out hello.mp3 svara voices --language hi ``` > The SDK installs from the repository until the first PyPI release lands; after that, `pip install svara-voice` is the same package and the same import. Extras use the same URL: `pip install "svara-voice[livekit] @ git+https://github.com/kenpath-labs/svara-python.git"`. ## OpenAI SDKs The official OpenAI Python and JavaScript SDKs work against Svara unmodified: point `base_url` at Svara and pass your key. Svara-specific fields ride in `extra_body` (Python) or as extra properties (JS). ```bash pip install openai ``` ```python from openai import OpenAI client = OpenAI(base_url="https://api.kenpathlabs.com/v1", api_key=SVARA_API_KEY) audio = client.audio.speech.create( model="svara-1", voice="sv_enhdbrj5", # Aanya input="Namaste!", response_format="mp3", extra_body={"lang": "hindi", "stream": False}, # svara extensions ) audio.write_to_file("out.mp3") ``` ```javascript import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.kenpathlabs.com/v1", apiKey: SVARA_API_KEY }); const res = await client.audio.speech.create({ model: "svara-1", voice: "sv_enhdbrj5", // Aanya input: "Namaste!", response_format: "mp3", // @ts-expect-error - svara extension lang: "hindi", }); ``` ## ElevenLabs SDKs The official ElevenLabs SDKs also work unmodified, including their realtime WebSocket client. Pass any non-empty `api_key`; its presence is also how `GET /v1/models` decides to answer in the ElevenLabs array shape. ```bash pip install elevenlabs ``` ```python from elevenlabs.client import ElevenLabs client = ElevenLabs(base_url="https://api.kenpathlabs.com", api_key=SVARA_API_KEY) audio = client.text_to_speech.convert( voice_id="sv_enhdbrj5", # Aanya text="Namaste!", model_id="eleven_multilingual_v2", # accepted, ignored output_format="mp3_44100_128", ) ``` ```javascript import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js"; const client = new ElevenLabsClient({ baseUrl: "https://api.kenpathlabs.com", apiKey: SVARA_API_KEY }); const stream = await client.textToSpeech.stream("sv_enhdbrj5", { text: "Namaste!" }); // voice: Aanya ``` ## Base URL conventions > Mind the `/v1`: **OpenAI SDKs include it** in the base URL (`https://api.kenpathlabs.com/v1`), while **ElevenLabs SDKs do not** (`https://api.kenpathlabs.com`); they append `v1/…` themselves. ## Compatibility surface What maps, and what to expect: - **Full support**: TTS + streaming, all output formats and rates, timestamps, the realtime WebSocket, voice list/search, models, languages. - **Honored**: `voice_settings.speed` maps onto the native speed parameter (their 0.7–1.2 range sits inside our 0.7–1.5) — on HTTP, with-timestamps and the realtime WebSocket alike. - **Accepted and ignored**: `voice_settings` (stability/similarity/style), `seed`, `model_id`, `previous_text`/`next_text`, request stitching. These don’t map onto this model; requests including them still succeed. (Pronunciation dictionaries _are_ supported — see [the guide](https://docs.kenpathlabs.com/pronunciation.md).) - **Stubbed**: user/subscription and voice-settings endpoints return valid, permissive shapes so SDK flows don’t break. A machine-readable OpenAPI document is served live at `https://api.kenpathlabs.com/openapi.json`: generate typed clients from it (Fern, Stainless, Scalar) for languages the official SDK doesn’t cover yet. --- # API reference Every endpoint, with links to the page that documents it. Every endpoint, with links to the page that documents it in full. The live OpenAPI spec is at `https://api.kenpathlabs.com/openapi.json`. ## Speech ```http POST /v1/audio/speech ``` Primary TTS: text in, audio out; set `stream` for a chunked response. See [Text to speech](https://docs.kenpathlabs.com/text-to-speech.md). ```http POST /v1/text-to-speech/{voice_id} ``` ```http POST /v1/text-to-speech/{voice_id}/stream ``` ```http POST /v1/text-to-speech/{voice_id}/with-timestamps ``` ElevenLabs-compatible synthesis, incl. streaming and character timestamps. `output_format` like `mp3_44100_128`, `pcm_24000`. See [SDKs](https://docs.kenpathlabs.com/sdks.md). ## WebSocket ```http WS /v1/audio/speech/stream-input ``` Native input streaming with `mode=eager`. Send text fragments, receive PCM. See [Input streaming](https://docs.kenpathlabs.com/input-streaming.md). ```http WS /v1/text-to-speech/{voice_id}/stream-input ``` ElevenLabs-compatible realtime WebSocket (BOS/text/flush/EOS protocol). ## Voices ```http GET /v1/voices ``` ```http GET /v2/voices ``` ```http GET /v1/voices/{voice_id} ``` ```http GET /v1/voices/{voice_id}/preview ``` Roster, search/paging, single voice, and bundled preview clip. ```http GET /v1/voices/{voice_id}/settings ``` Stub for ElevenLabs SDK compatibility: returns permissive defaults so client flows that read voice settings don’t break. ## Utility ```http GET /health ``` Capability negotiation: `engine`, `stream_formats`, `default_voice`. No auth. ```http GET /v1/languages ``` ```http GET /v1/models ``` Supported languages and models. No auth. ```http GET /v1/usage ``` ```http GET /v1/user/subscription ``` Your plan, balance and month-to-date usage — the second is the ElevenLabs-shaped dialect. See [Usage & subscription](https://docs.kenpathlabs.com/usage-api.md).