Other clients

xAI API drop-in

xAI's own text-to-speech API, REST and WebSocket, through the cache: change the base URL and key.

WebSocket#

xAI’s text-to-speech API, mirrored under https://api.ttscacherouter.dev/xai. Use xAI’s URLs with https://api.x.ai replaced by https://api.ttscacherouter.dev/xai, and your router API key in Authorization: Bearer.

speak.py
import asyncioimport base64import jsonimport osimport websocketsasync def main():    uri = "wss://api.ttscacherouter.dev/xai/v1/tts?voice=ara&language=en&codec=pcm&sample_rate=24000"    headers = {"Authorization": f"Bearer {os.environ['TTS_CACHE_ROUTER_API_KEY']}"}    async with websockets.connect(uri, additional_headers=headers) as ws:        await ws.send(json.dumps({"type": "text.delta", "delta": "Your order has shipped. "}))        await ws.send(json.dumps({"type": "text.delta", "delta": "It arrives on Friday."}))        await ws.send(json.dumps({"type": "text.done"}))        audio = bytearray()        async for message in ws:            event = json.loads(message)            if event["type"] == "audio.delta":                audio.extend(base64.b64decode(event["delta"]))            elif event["type"] == "audio.done":                break            elif event["type"] == "error":                raise RuntimeError(event["message"])    with open("speech.pcm", "wb") as f:        f.write(audio)asyncio.run(main())

wss://api.ttscacherouter.dev/xai/v1/tts takes xAI’s query (voice, language, codec, sample_rate, bit_rate, speed, optimize_streaming_latency, text_normalization) and refuses a bad one at the handshake, as xAI does. Send text.delta messages and end each utterance with text.done; you get audio.delta messages, then audio.done. text.clear stops the utterance and is answered with audio.clear. Requests go to xAI with the xAI key stored under Provider keys. For a LiveKit agent, see xAI with LiveKit.

The router splits your text into sentences and serves each sentence from the cache or synthesizes it at xAI, so the messages are xAI’s with these differences:

  • Audio for a sentence starts once the sentence is complete.
  • text.delta messages are joined exactly as sent, as xAI joins them: put the spaces between words in your text.
  • audio.done’s trace_id is the router’s own.
  • with_timestamps=true is refused at the handshake, and session.update (pronunciation replacements) is answered with an error message: use the REST endpoint for either. codec=opus is refused.
  • With codec=wav, each utterance starts with one WAV header, as from xAI.
  • A failure at xAI (an unknown voice, your xAI quota) arrives as an error message once a sentence needs xAI, then the router closes the connection (code 1011). A connection idle for 30 minutes is closed.

REST#

  • POST /xai/v1/tts: the audio in the codec the body asks for (MP3 when it doesn’t), with xAI’s own Content-Type; JSON with with_timestamps. replace works as at xAI.
  • GET /xai/v1/tts/voices and GET /xai/v1/tts/voices/{voice_id}: xAI’s voices, not cached.

Any other path under /xai answers 404.

curl "https://api.ttscacherouter.dev/xai/v1/tts" \  -H "Authorization: Bearer $TTS_CACHE_ROUTER_API_KEY" \  -H "Content-Type: application/json" \  -d '{"text": "Your order has shipped.", "voice_id": "eve", "language": "en"}' \  --output speech.mp3

What is cached#

  • REST: each request as a whole. The same body again replays the same audio.
  • WebSocket: each sentence, so a sentence you have sent before with the same query comes from the cache whatever came around it. Sentences share the cache with the router’s own /v1/stream when the settings are the same.
  • A reply from the cache carries only Content-Type: no xAI request produced it, so there is no x-trace-id.

Errors#

A wrong router key gets xAI’s own refusal, HTTP 400, and a missing one HTTP 401, on REST and at the WebSocket handshake. An org with no xAI key stored gets the same 400, with a message that links to the dashboard. Errors from xAI, such as an unknown voice, reach you as xAI sent them.

HTTP/1.1 400 Bad Request{"code": "Client specified an invalid argument", "error": "Incorrect API key provided."}