Getting started

One origin serves REST, Server-Sent Events and WebSocket over HTTPS. Every request carries an API key. Send text, get back raw audio.

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

Which endpoint

UseEndpointBest for
WebSocketwss://…/wsVoicebots and live calls: audio starts in tens of milliseconds, one socket for many turns.
StreamingPOST /api/synthesize/streamSame progressive audio over plain HTTP (SSE).
Single callPOST /api/synthesizeBatch jobs, pre-rendered prompts, tests: one request, one audio buffer.

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": {...}}:

eventdata
startrequest_id, sample_rate of every audio byte that follows
audiochunk (base64 audio), offset, total; the first piece of each clause also has word_timestamps and the spoken text
word_timestampstimings for the whole utterance, text, duration
completeduration, generation_time, total_bytes: the end of the turn
errorerror message for this turn

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

FieldDefaultNotes
textrequiredPlain prose, not empty. Send it as a person would write it: numbers, dates, currency and romanised Hindi are handled for you. No SSML, markdown or emoji.
ref_idservice defaultVoice id, from the list below. Always send it explicitly.
speed1.00.5 to 1.5. Values outside the range are rejected with a 400.
sample_rate80008000, 16000, 22050 or 24000 Hz.
request_idgeneratedYour correlation id. It is echoed back and shown in Generations. Reuse it when you retry.
return_base64falsePOST /api/synthesize only: JSON with base64 audio instead of a binary body.

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.

idgenderlanguage

Errors and retries

StatusMeaningDo
400Invalid request: empty text, bad JSON, voice, speed or sample rateFix the request; do not retry.
403Missing or wrong keyCheck the key and header; do not retry.
405Wrong method (synthesis is POST only)Use POST.
413Body over 1 MBSend shorter text.
500, 502, 503Synthesis failed or temporarily unavailableRetry with backoff.

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

Last request

First audio—ms
Total time—ms
audio—s
realtime—×
parts—
gaps—

Getting started

Stream audio over a WebSocket and get transcripts back as you speak. The API follows Deepgram's live protocol, so existing clients only need a new URL and key.

URL and key

Connect to and send your key as Authorization: Token <key>. From a browser, which cannot set headers, use the subprotocol: new WebSocket(url, ['token', key]).

Send raw 16-bit little-endian mono PCM as binary messages, in real time, and close with {"type":"CloseStream"}.

Python

pip install websockets
import asyncio, json, wave, websockets

URL = "{{WS}}?encoding=linear16&sample_rate=8000&interim_results=true"
KEY = "YOUR_API_KEY"

async def main():
    with wave.open("call.wav") as w:           # 16-bit mono, 8 kHz
        audio = w.readframes(w.getnframes())
    headers = {"Authorization": f"Token {KEY}"}
    async with websockets.connect(URL, additional_headers=headers) as ws:
        async def send():
            for i in range(0, len(audio), 1600):       # 100 ms at a time
                await ws.send(audio[i:i + 1600])
                await asyncio.sleep(0.1)
            await ws.send('{"type":"CloseStream"}')
        sender = asyncio.create_task(send())
        async for raw in ws:
            m = json.loads(raw)
            if m["type"] == "Results":
                text = m["channel"]["alternatives"][0]["transcript"]
                print("final  " if m["is_final"] else "interim", text)
            elif m["type"] == "Metadata":
                print("billed audio seconds:", m["duration"])
        await sender

asyncio.run(main())

Query parameters

ParameterDefaultNotes
sample_raterequired8000 or 16000.
encodinglinear16The only encoding; anything else is refused.
channels1Mono only.
modelyetiThe only model; any other name is refused.
languagehiOne of hi en auto as bho bn gu kn mai ml mr or pa ta te ur. Sets the script of the text: hi writes Devanagari, en writes Latin letters. auto lets the engine choose the language. Lower case only; if repeated, the last one counts.
interim_resultsfalseSend partial transcripts while speech continues.
utterance_end_msoffAt least 1000; needs interim_results=true.
vad_eventsfalseSend SpeechStarted.
tag, extranoneEchoed in Metadata. tag may repeat. extra is key:value and may repeat.
punctuate and unknown names—Accepted and ignored.
Features not offered—Refused with 400 when set to anything but false: numerals smart_format diarize keywords keyterm profanity_filter redact search replace filler_words detect_language dictation measurements paragraphs summarize topics intents sentiment. endpointing is refused with any value.

Messages

MessageDirectionMeaning
Resultsfrom serverA transcript. is_final marks text that will not change; speech_final means the speaker paused; from_finalize marks a final caused by Finalize.
Metadatafrom serverSent once, last, with the audio's duration and your tags and extra.
SpeechStarted, UtteranceEndfrom serverOnly when asked for.
{"type":"Finalize"}from clientFinish the current segment now; its final arrives with from_finalize.
{"type":"KeepAlive"}from clientKeep a silent stream open. With no audio or message for 10 s the session closes.
{"type":"CloseStream"}from clientFinish and close.

Close codes

CodeMeaningDo
1000Finished after CloseStreamNothing.
1009A message was over 256 KiBSend smaller chunks, such as 100 ms of audio.
1011 NET-0001No audio or message for 10 sSend KeepAlive while silent.
1011 NET-0000The engine failedReconnect and retry.
1012The server is restartingReconnect.
idle

Transcript

Pick a file or start the microphone.

This session

Connect—ms
First text—ms
Last final—ms
Audio—s
Close—

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.

idle

Reply

Write a message and press Send. The reply streams in.

This request

First token—ms
Total—ms
Speed—tokens/s
Tokens in—
Tokens out—
Finish—

Clone a voice

Upload a clean recording of one speaker (3 to 20 seconds) and exactly what it says. Try it first, then save it as a voice of your own.

Your cloned voices

NameIdLanguage

Totals

Usage

By type

Requests