# Gandr TTS — complete API documentation (machine-readable) # API: https://tts.gandr.ai · docs: https://gandr.ai/docs · contact@gandr.ai # This one file is the full developer documentation, written to be consumed by # AI coding assistants. Paste it into Claude, ChatGPT, Cursor or Copilot and # say "integrate Gandr TTS into this project". # OpenAPI: https://gandr.ai/openapi.yml · AsyncAPI: https://gandr.ai/asyncapi.yml # LiveKit guide: https://gandr.ai/integrations/livekit # Single-file SDKs: https://gandr.ai/sdk/gandr_tts.py · /sdk/gandr-tts.js # LiveKit plugin: /integrations/livekit/gandr_tts.py # Last verified against the production API: 2026-07-31 ## Overview Real-time text-to-speech with instant voice cloning, gap-free streaming at any length, multilingual, served from US and EU regions with automatic nearest- region routing. Three synthesis endpoints share one request shape. Time to first audio, measured on the production API 2026-07-31, five runs per rate per voice on warm workers, server-side (the `ttfa_ms` the API returns): p50 184ms at 24000 Hz, 164ms at 22050, 182ms at 16000 — and 394ms at 8000. IMPORTANT: only 8000 Hz is slow. Resampling to 8k happens server-side BEFORE the first chunk ships and costs roughly 220ms every utterance. Request 24000 (the default) even for telephony and let your SIP stack downsample locally, where it is free. If you need a narrowband source, 16000 costs nothing. ## Base URL and auth Base URL: https://tts.gandr.ai Auth (every request): header `x-api-key: gnd_...` OR `Authorization: Bearer gnd_...` ## Endpoints 1) WebSocket wss://tts.gandr.ai/ws — live calls/agents. Lowest latency. 2) POST /v1/tts/sse — HTTP streaming (Server-Sent Events). 3) POST /v1/tts/bytes — one-shot audio/wav render (quality lane). 4) POST /v1/vapi — Vapi custom-voice contract (see Integrations). 5) GET /v1/voices — list stock voices. 6) GET /v1/usage — month-to-date characters + requests for your key. 7) GET /v1/prewarm — boots a worker in the background; returns instantly. ## 60-second quickstart (curl) curl -X POST https://tts.gandr.ai/v1/tts/bytes \ -H "x-api-key: gnd_your_key" -H "content-type: application/json" \ -d '{"transcript":"Hello from Gandr.","language":"en", "voice":{"mode":"id","id":"gandr-mia"}, "output_format":{"sample_rate":24000}}' \ --output hello.wav ## Quickstart (Python, no SDK) import requests r = requests.post("https://tts.gandr.ai/v1/tts/bytes", headers={"x-api-key": "gnd_your_key"}, json={"transcript": "Hello from Gandr.", "language": "en", "voice": {"mode": "id", "id": "gandr-mia"}, "output_format": {"sample_rate": 24000}}) open("hello.wav", "wb").write(r.content) ## Integrations (pick your stack) LiveKit Agents (Python) — full guide https://gandr.ai/integrations/livekit Download the single-file plugin: https://gandr.ai/integrations/livekit/gandr_tts.py from gandr_tts import GandrTTS session = AgentSession(tts=GandrTTS(voice="gandr-mia"), ...) # key from GANDR_API_KEY env Constructor: api_key, voice, lang, sample_rate (8000/16000/22050/24000), speed (0.6-1.5), volume (0.5-2.0), extra (dict merged into the request body, e.g. pronunciation_dict), base_url, timeout, http_session. Runtime: session.tts.update_options(voice=..., lang=..., speed=..., volume=...) Fallback: wrap in livekit.agents.tts.FallbackAdapter([GandrTTS(...), ]). Pipecat (Python) Download: https://gandr.ai/sdk/gandr_pipecat.py from gandr_pipecat import GandrTTSService tts = GandrTTSService(api_key="gnd_...", voice="gandr-mia", word_timestamps=True) pipeline = Pipeline([..., llm, tts, transport.output(), ...]) Vapi (no code) Assistant → Voice → custom-voice, set two fields: URL: https://tts.gandr.ai/v1/vapi?voice=gandr-mia Secret: your gnd_ key (Vapi sends it as the X-VAPI-SECRET header) The endpoint implements Vapi's custom-voice contract natively: it receives {"message":{"type":"voice-request","text":...,"sampleRate":...}} and answers raw headerless PCM s16le mono at the requested sampleRate (8000/16000/22050/ 24000; telephony uses 8000). Swap the voice via the ?voice= query param. One-click setup UI (finds your assistants, configures them): /access.html Single-file SDKs (anything else) Python: https://gandr.ai/sdk/gandr_tts.py (pip install websockets requests) Node: https://gandr.ai/sdk/gandr-tts.js (Node 18+, npm install ws) from gandr_tts import GandrTTS tts = GandrTTS("gnd_your_key"); tts.prewarm() wav = tts.say("Hello there.", voice_id="gandr-mia") # or voice_wav="ref.wav" to clone for pcm in tts.stream("Live call speech.", voice_id="gandr-mia"): play(pcm) # Node: say/stream take { voiceId: "gandr-mia" } or { voiceWav: "ref.wav" } # per-utterance extras + final stats (timestamps, ttfa_ms, audio_ms): for pcm in tts.stream("...", extra={"add_timestamps": "word"}): play(pcm) tts.last_stats["word_timestamps"] # Node: { extra: {...} } -> tts.lastStats Raw REST/WebSocket — full shapes below; specs at /openapi.yml + /asyncapi.yml. ## Voice (required on every synthesis request) Clone per request (instant, no upload step): "voice": {"mode": "clone", "wav_b64": ""} The reference is fingerprinted and cached — repeat calls with the same clip skip re-cloning, so cloned voices serve at stock-voice latency after first use. Stock voices (multilingual; no reference needed): "voice": {"mode": "id", "id": "gandr-mia"} gandr-mia (warm, natural, f — flagship) · gandr-ava (warm, friendly, f) gandr-jenny (warm, natural, f) · gandr-dane (smooth, measured, m) gandr-leo (clear, professional, m) · gandr-lewis (warm, even, m) List programmatically: GET /v1/voices -> {"voices":[{"id":"gandr-ava","name":"Ava","language":"multilingual"}, ...]} WebSocket uses flat fields instead: "voice_id": "gandr-mia" or "voice_wav_b64" (first turn only). ## REST request body (both /v1/tts/bytes and /v1/tts/sse) { "transcript": "Hello from Gandr.", // required (max 2000 chars) "language": "en", // en verified; more in beta — see /tts/languages/ "voice": {"mode": "id", "id": "gandr-ava"}, // or {"mode":"clone","wav_b64":"..."} "output_format": {"sample_rate": 24000}, // 8000 / 16000 / 22050 / 24000 "expressiveness": 0.3, // prosody energy (default 0.3 flat/professional; ~0.9-1.0 + low cfg = livelier) "temperature": 0.45, // prosodic variation 0.1..1.2 (default 0.45) "cfg_weight": 0.6, // guidance/pacing 0.2 spacious .. 1.0 brisk (default 0.6) "speed": 1.0, // 0.6..1.5, pitch-preserving, post-synthesis "volume": 1.0, // 0.5..2.0, soft-ceiling mastered, never clips "add_timestamps": true, // optional: true/"word" = words; "char"/"all" adds char spans "pronunciation_dict": [ // per-request sounds-like replacements {"text": "tchoupitoulas", "pronunciation": "chop-uh-TOO-liss"} ] } "text" is accepted as an alias for "transcript"; "lang" for "language". ## Responses /v1/tts/bytes: 200 with Content-Type audio/wav (mono PCM16 at the requested rate). /v1/tts/sse: text/event-stream; each audio event is data: {"data": ""} final event carries render stats: data: {"done": true, "ttfa_ms": , "audio_ms": } with "add_timestamps" set, the final event also carries timings (seconds from audio start, aligned against the rendered audio, computed after the last audio chunk — zero streaming-latency cost): "word_timestamps": {"words": [...], "start": [...], "end": [...]} "add_timestamps": "char" or "all" additionally returns "char_timestamps": {"chars": [...], "start": [...], "end": [...]} Alignment is production-verified for English. WebSocket: wss://tts.gandr.ai/ws with header Authorization: Bearer gnd_... Send one JSON message per utterance: {"text": "...", "lang": "en", "voice_id": "...", "voice_wav_b64": "", "output_sample_rate": 24000, "add_timestamps": "word"} Receive binary PCM16LE frames (mono, at output_sample_rate), then a final JSON line {"ttfa_ms":.., "audio_ms":..} (+"truncated": true if a stream ends early — retry the remainder). add_timestamps works exactly like SSE: the final JSON line carries word_timestamps / char_timestamps with zero added latency on live calls. /v1/vapi: 200 with application/octet-stream — raw headerless PCM s16le mono at the requested sampleRate. (This endpoint exists for Vapi's contract; everything else should use bytes/sse/ws.) ## Transcript controls (all endpoints) Automatic readback normalization (English, no markup): times -> "nine A M", money/percent -> words, codes OX49 -> "O X, four nine", phone-shaped 555-0142 -> digit-by-digit, long IDs/zips/tracking -> digit-by-digit. TKT4829XB — character-by-character read (codes, IDs, serials) — natural pause at that point (duration advisory) pronunciation_dict (body field) — see schema above; lowercase entries also match sentence-start capitalization ## Expression recipe Defaults produce a flat, professional read. Slower/spacious: lower cfg_weight. Wider melodic range: raise temperature. Livelier overall: {"expressiveness": 0.9, "cfg_weight": 0.3, "temperature": 0.8} ## Cold start / prewarm Workers scale to zero when idle; a cold worker starts serving in ~25s and reaches full speed in the background shortly after. GET /v1/prewarm returns instantly and boots a worker in the background — fire it when a call starts (e.g. SIP invite) and first audio is full-speed by the time you synthesize. Both SDKs auto-prewarm and retry through a cold wake. For agent frameworks, a fallback adapter (see LiveKit above) covers the cold edge cleanly. ## Fair use Streams are serialized (one utterance at a time per stream). For batch or render-farm workloads, contact contact@gandr.ai. ## Errors 400 malformed body (JSON names the field) · 401 bad/missing key · 429 per-key rate limit (120 req/min, raised on request) or monthly quota · 5xx transient — retry once. A stream that ended early carries "truncated": true in its final event: TREAT THAT AS A FAILURE AND RETRY the utterance rather than playing what arrived. Half a sentence read confidently is worse than a retry, and on a clinical or financial call it is much worse. Identical requests may be served from cache, so retries are safe. (Renders are non-deterministic - there is no seed parameter.) ## Security & status Security & data handling: /security.html Live status: /status/ All generated audio carries an inaudible machine-readable watermark. ## Getting a key https://gandr.ai/waitlist/ — takes ~20 seconds; keys go out within the hour. ## Try it interactively /access.html — paste your key once: audition every stock voice, one-click Vapi assistant setup, plugin downloads for LiveKit and Pipecat.