# 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-08-24 ## Overview Real-time text-to-speech with instant voice cloning, gap-free streaming at any length, multilingual, with automatic nearest-region routing. Three synthesis endpoints share one request shape. First-audio latency: first audio byte in 146 ms over the open internet, 116 ms p50 first audio, server side warm. What also holds and is freshly measured: gap-free streaming. Production outruns playback, so audio starts before the render finishes, the buffer grows as the utterance runs, and longer text is safer, not riskier. Every utterance also returns its own ttfa_ms, so your integration can measure itself on your own path. ## The publishable numbers (canonical, mirrors gandr.ai/pricing) These are the only numbers Gandr publishes; quote them as written, with the vantage attached. Source of truth: https://gandr.ai/pricing, same-day. No client-side latency figure is published at present; the server-side basis in the Overview above is the only latency statement, and it never travels without that basis. - Word error rate 1.98% against a 2.17% human reference, same scorer. - 23 languages, one engine, every voice. - Every render watermarked, everywhere. - No user data in any training set. - Prices: Free $0 for 50,000 tokens. $10/mo for 1M tokens, resets monthly. $50/mo for 5M. $150/mo per unlimited, unmetered stream ($180 month to month). Enterprise from 500 lines. The unit is tokens; one token is one character. Time to first audio BY REQUESTED RATE: 24000, 22050 and 16000 Hz all land close together. 8000 Hz is materially slower to first audio than every other rate (measured 2026-07-31, five runs per rate per voice on warm workers). 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. 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) POST /v1/key/usage , the ledger read: quota_chars, chars_used, remaining. 8) GET /v1/prewarm , boots a worker in the background; returns instantly. 9) POST /v1/audio/speech , OpenAI-compatible speech: point any OpenAI SDK's base_url at https://tts.gandr.ai/v1. Formats mp3, wav, pcm; the default is mp3, exactly like OpenAI. ## 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) MCP server (text to speech inside any MCP agent: Claude, Cursor, etc.) pip install gandr-mcp # then add to your MCP client's config: {"mcpServers":{"gandr":{"command":"gandr-mcp","env":{"GANDR_API_KEY":"gnd_..."}}}} Tools: synthesize (text to WAV base64, voice/language/sample-rate/pitch/pacing), list_voices, list_languages (23), get_usage (tokens used by the key). Repo: https://github.com/Gandr-AI/gandr-mcp Official SDKs (published packages) Python: pip install gandr (Python 3.9+, zero dependencies) Node: npm install gandr (Node 18+, zero dependencies) from gandr import Gandr; g = Gandr("gnd_..."); g.say("Hello.") Framework plugins LangChain: pip install gandr-langchain -> GandrText2SpeechTool() CrewAI: pip install gandr-crewai -> GandrTTSTool() Haystack: pip install gandr-haystack -> GandrTTS() pipeline component AI SDK (JS): npm install gandr-ai-sdk ai -> createGandr() + generateSpeech() n8n: Settings -> Community nodes -> n8n-nodes-gandr Dify: plugin at github.com/Gandr-AI/dify-plugin (tool node; gnd_ key credential) Repos: https://github.com/Gandr-AI (gandr-mcp, gandr-langchain, gandr-crewai, gandr-haystack, gandr-ai-sdk, n8n-nodes-gandr, gandr-livekit, gandr-pipecat, examples, community-integrations) 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) Install: uv add pipecat-gandr (documented community integration: docs.pipecat.ai/api-reference/server/services/tts/gandr) from pipecat_gandr import GandrTTSService tts = GandrTTSService( api_key=os.environ["GANDR_API_KEY"], params=GandrTTSService.InputParams(voice_id="gandr-mia"), ) InputParams: voice_id, language, sample_rate (8000/16000/22050/24000), speed (0.6-1.5), volume (0.5-2.0), temperature, cfg_weight, seed, voice_wav_b64 (clone reference, sent on the first utterance per connection), word_timestamps (True asks the door for per-word offsets, fed into Pipecat's word-timestamp machinery). One WebSocket held across turns; utterances serialized in order; an interruption drops queued speech and reopens the connection. Pipecat legacy single-file service (per-request HTTP): https://gandr.ai/sdk/gandr_pipecat.py GandrTTSService(api_key="gnd_...", voice="gandr-mia", word_timestamps=True) word_timestamps=True sets add_timestamps on the request; the utterance tail then carries word-level timing pairs for captions or alignment. 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): https://gandr.ai/quickstart/ 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 (easy, unhurried, f, flagship) · gandr-ava (warm, friendly, f) gandr-jenny (steady, sincere, 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 https://gandr.ai/languages/ "voice": {"mode": "id", "id": "gandr-ava"}, // or {"mode":"clone","wav_b64":"..."} "output_format": {"sample_rate": 24000}, // 8000 / 16000 / 22050 / 24000 "temperature": 0.9, // prosodic variation (melody) 0.1..1.2; omit it and the door fills it in per voice (see Expression recipe) "cfg_weight": 0.3, // guidance/pacing 0.2 spacious .. 1.0 brisk; omit it and the door sends nothing "speed": 1.0, // 0.6..1.5, pitch-preserving, post-synthesis "volume": 1.0, // 0.5..2.0, soft-ceiling mastered, never clips "seed": 42, // optional: same seed + text + voice + params -> the same audio "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". Two fields are deliberately absent from the body above, and neither belongs in code you write today: "expressiveness" is ACCEPTED AND INERT (remapped off the engine 2026-07-29; measured +0.33 semitones, p=0.804) and has no default, because the door never injects it; and "emotion" is RETIRED, the door discards it before synthesis. Sending either changes nothing you can hear, and a request that carries either one still succeeds, if your code sends them today, it keeps working; it just is not steering anything. They differ in the one way that matters if you cache or diff by seed: "emotion" is discarded, so the bytes come back identical, while "expressiveness" is still forwarded and perturbs the sample, at a held seed, two different values return different audio. Treat it as a second seed, not a dial. ## 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): "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 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 Two dials move a read on this engine, and expressiveness is not one of them (it was remapped off the engine on 2026-07-29; measured effect +0.33 semitones, p=0.804). Slower and more spacious: lower cfg_weight. Wider melodic range: raise temperature. Raise the melody and loosen the guidance together, so the bigger delivery stays unhurried: {"temperature": 0.9, "cfg_weight": 0.3} SEND THEM, because what you get by omitting them is not one house setting. temperature is the ONE expression field the door fills in, and it fills it in per voice: stock jenny/ava/mia/lewis 0.5, dane 0.65, leo 0.8, the door's floor for a voice its map does not name (read off the live door env, all three regions, 2026-08-06). What a CLONE inherits was not part of that read, so send temperature explicitly when you clone. Omit cfg_weight and the door forwards nothing, the engine rests at its own 0.5. Omit expressiveness and nothing is sent either; it has no default and is inert whatever you send, inert, not discarded: at a held seed two values return different audio. `emotion` is retired: the door discards the field before synthesis, and the same request at a held seed returns identical audio whichever emotion is named. Shape the read with the transcript, temperature and cfg_weight. ## Overflow / prewarm The fleet is always on. Overflow spills to a fallback lane that can take longer on its first request, then reaches full speed shortly after. GET /v1/prewarm returns instantly and opens the path 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 an overflow response. For agent frameworks, a fallback adapter (see LiveKit above) covers that edge cleanly. ## Concurrency Streams are serialized (one utterance at a time per stream). For batch or render-farm workloads, contact contact@gandr.ai. ## Errors 400 {"error":"bad_json"} or a message naming the field · 401 {"error":"invalid_api_key"} · 402 {"error":"quota_exceeded","used_chars":..,"quota_chars":..,"requested_chars":..} · 429 {"error":"rate_limited"} per-key rate limit (120 req/min, raised on request) · 503 {"error":"at_capacity"}, retry with backoff. 402 IS THE ONE TO HANDLE: it means the characters this key was bought with are spent. It is 402 and not 429 deliberately, it is not a rate limit and retrying does not help; buy more characters, or move to a stream. A stream key is not billed by the character and is unlimited and unmetered, so 402 is not a code you should plan around on one. The count is kept per door, so treat any remaining figure as approximate. The WebSocket answers the same errors in band, on an open socket, rather than closing it. 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; pass "seed" when you need the same audio back deliberately. ## Security & status Security & data handling: https://gandr.ai/security/ Live status: /status/ All generated audio carries an inaudible machine-readable watermark. ## Getting a key https://gandr.ai/join/, sign in and claim the free key on your console: 50,000 tokens (one token is one character), one per person. Paid keys land the same way, on the dashboard the moment payment settles. From 500 lines: https://gandr.ai/waitlist/, where we size the fleet with you first. ## Try it interactively https://gandr.ai/quickstart/, paste your key once: audition every stock voice, one-click Vapi assistant setup, plugin downloads for LiveKit and Pipecat. (The old /access.html console on the API host is retired; every human page there now redirects to gandr.ai.)