Base URL and key
Base URL: . Send your key in the X-API-Key header (or Authorization: Bearer <key>). Keep it on your server: never ship it in a web page or mobile app. A missing or wrong key returns 403.
Check that your key works:
BASE={{BASE}}
KEY=YOUR_API_KEY
curl -s $BASE/api/health -H "X-API-Key: $KEY"
curl -s -o /dev/null -w '%{http_code}\n' $BASE/ready -H "X-API-Key: $KEY" # 200 = ready to speak
REST: one call, one clip
Without return_base64 the response body is the raw audio and the details come back in headers (X-Audio-Duration, X-Sample-Rate, X-Request-ID).
curl -s -X POST $BASE/api/synthesize \
-H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
-d '{"text":"Hello! Your order is ready.","ref_id":"8","request_id":"order-1001"}' \
-o out.pcm
ffmpeg -f s16le -ar 8000 -ac 1 -i out.pcm out.wav # raw audio has no header; wrap it to play it
import requests, wave
BASE, KEY = "{{BASE}}", "YOUR_API_KEY"
r = requests.post(
f"{BASE}/api/synthesize",
headers={"X-API-Key": KEY},
json={"text": "Hello! Your order is ready.", "ref_id": "8",
"sample_rate": 16000, "request_id": "order-1001"},
timeout=60,
)
r.raise_for_status()
rate = int(r.headers["X-Sample-Rate"])
with wave.open("out.wav", "wb") as w: # wrap the raw 16-bit mono audio in a WAV header
w.setnchannels(1); w.setsampwidth(2); w.setframerate(rate)
w.writeframes(r.content)
print(r.headers["X-Audio-Duration"], "seconds of audio")
Add "return_base64": true to get JSON instead: audio_data (base64), word_timestamps, duration, generation_time, request_id, sample_rate.
Streaming over HTTP (SSE)
Same request body. Events arrive as the audio is produced: start, many audio, word_timestamps, then complete. An error event ends the stream.
import base64, json, requests
BASE, KEY = "{{BASE}}", "YOUR_API_KEY"
audio = bytearray()
with requests.post(f"{BASE}/api/synthesize/stream",
headers={"X-API-Key": KEY},
json={"text": "Hello! Your order is ready.", "ref_id": "8"},
stream=True, timeout=60) as r:
r.raise_for_status()
event = None
for line in r.iter_lines(decode_unicode=True):
if line.startswith("event: "):
event = line[7:]
elif line.startswith("data: "):
d = json.loads(line[6:])
if event == "audio":
audio += base64.b64decode(d["chunk"]) # hand each piece to your player
elif event == "complete":
print(f'{d["duration"]:.2f}s of audio, {d["total_bytes"]} bytes')
elif event == "error":
raise RuntimeError(d["error"])
WebSocket: the low-latency path
Connect once to and keep the socket open for the whole call. Send one JSON message per utterance and read its events until complete. After an error the socket stays usable, so send the next utterance on the same connection.
pip install websockets
import asyncio, base64, json, websockets
URL, KEY = "{{WS}}", "YOUR_API_KEY"
async def say(ws, text, ref_id="8", **options):
await ws.send(json.dumps({"text": text, "ref_id": ref_id, **options}))
audio = bytearray()
async for raw in ws:
msg = json.loads(raw)
event, data = msg["event"], msg["data"]
if event == "audio":
audio += base64.b64decode(data["chunk"]) # play as it arrives
elif event == "complete":
return bytes(audio), data["duration"]
elif event == "error":
raise RuntimeError(data["error"])
async def main():
async with websockets.connect(URL, additional_headers={"X-API-Key": KEY}) as ws:
for turn in ["Hello! How can I help?", "Your balance is two thousand rupees."]:
pcm, seconds = await say(ws, turn, sample_rate=16000)
print(f"{len(pcm)} bytes, {seconds:.2f}s")
asyncio.run(main())
Events, each a JSON text frame {"event": ..., "data": {...}}:
From a browser the handshake cannot carry headers: send the key as a subprotocol, new WebSocket(url, ['token', key]), or have your backend proxy the socket. Idle sockets are closed after a short while; reconnect when that happens.
Request options
The language is detected from the text. Do not split long text yourself: it is chunked on the server, with a short first piece so audio starts quickly. Limits: 1 MB per request, and about 5,000 bytes of text (roughly 1,600 Devanagari characters) for POST /api/synthesize.
Audio
Every endpoint returns raw PCM: signed 16-bit little-endian, mono, no header, at the sample_rate you asked for (8 kHz by default). Do not add gain: the level is already right. Duration in seconds is len(pcm) / (2 * sample_rate).
import wave
def save_wav(path, pcm: bytes, rate: int = 8000):
with wave.open(path, "wb") as w:
w.setnchannels(1); w.setsampwidth(2); w.setframerate(rate)
w.writeframes(pcm)
Word timings are a flat list in milliseconds, [start, end, start, end, ...], over the spoken text. Read them in pairs: list(zip(ts[0::2], ts[1::2])).
Voices
Pass a voice id as ref_id. The same id always gives the same voice. Fetch the live list with GET /voices.json instead of hard-coding ids.
Errors and retries
A 400 body names the problem: {"code": "speed_out_of_range", "message": "...", "field": "speed", "request_id": "..."}. On the streaming paths errors arrive as an error event.
import random, time, requests
def synthesize(text, ref_id="8", attempts=3):
for i in range(attempts):
try:
r = requests.post(f"{BASE}/api/synthesize", headers={"X-API-Key": KEY}, timeout=60,
json={"text": text, "ref_id": ref_id, "request_id": "job-42"})
if r.status_code == 200:
return r.content
if r.status_code in (400, 403, 405, 413):
r.raise_for_status() # your request is wrong: retrying will not help
except requests.Timeout:
pass
time.sleep(2 ** i + random.random()) # backoff: 1s, 2s, 4s plus jitter
raise RuntimeError("synthesis failed")
Checklist
- Key stays on your server and goes in a header, never in a URL you log.
- Always send
ref_id and your own request_id.
- Treat audio as mono 16-bit PCM at the rate you requested. No gain, no chunking of your text.
- Reuse connections: one WebSocket per call,
requests.Session for REST.
- Time out streams on silence (about 30 s), not on total duration.
- Retry 5xx and timeouts with backoff, never 400 or 403.
- Watch your usage and response times in the Stats tab, and find any request by its id in Generations.
Getting started
Chat completions over the OpenAI API: existing OpenAI clients only need a new base URL and key. Streaming is supported and recommended.
URL and key
Send requests to /v1/chat/completions with Authorization: Bearer <key>. GET /v1/models lists the model names.
Available models
Set model to one of these. A request for any other name gets a 404. Loading...
Streaming
curl {{BASE}}/v1/chat/completions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Session-Id: call-123" \
-d '{
"model": "{{MODEL}}",
"messages": [{"role": "user", "content": "Hello"}],
"stream": true
}'
Python
pip install openai
from openai import OpenAI
client = OpenAI(base_url="{{BASE}}/v1", api_key="YOUR_API_KEY")
stream = client.chat.completions.create(
model="{{MODEL}}",
messages=[{"role": "user", "content": "Hello"}],
stream=True,
extra_headers={"X-Session-Id": "call-123"},
)
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="")
Keeping a conversation on one engine
Send the header X-Session-Id with one stable value per conversation or call. Requests that share it go to the same engine, which keeps its prompt cache warm. Without it, requests spread over the engines.
Add X-Meta-<name>: <value> headers to label a request, for example X-Meta-Team: voice. Each is stored with the request as name:value (up to 16, values up to 256 characters) so you can filter by it later. Do not put personal data in them.
Errors
Errors use the OpenAI shape: {"error":{"message","type","param","code"}}. 401 invalid_api_key: the key is wrong. 413 request_too_large: the body is over 6 MiB. 503 service_unavailable: no engine could take the request. A failed attempt is retried on another engine before any output reaches you.