Skip to main content
Lesson 13 of 14 · Text-to-Speech API

HTTP errors and API key security

Lesson 13 of 14 Updated November 3, 2026 Developer
On this page
  1. Handle errors and protect your credentials
  2. HTTP error codes
  3. Never expose the API key on the client
  4. Fail-open moderation
  5. Key takeaways

Updated November 3, 2026, based on the xAI Text to Speech and Rate limits pages: text limited to 60,000 characters, the 404 error (unknown voice), and rate limits by tier. Sources: Text to Speech · Rate limits.

Handle errors and protect your credentials

In production, your app needs to handle TTS API errors gracefully and, above all, never expose your API key on the client. This lesson covers error codes, retry strategies, and the backend proxy architecture.

HTTP error codes

The TTS API returns standard HTTP error codes. Here’s how to react to each one.

400: Bad Request

The server couldn’t process your request. Common causes:

  • Empty text or text over 60,000 characters
  • An invalid codec (a typo in the name)
  • An unsupported sample rate
if response.status_code == 400:
    error = response.json()
    print(f"Invalid request: {error}")
    # Fix the parameters and try again

Don’t retry a 400 error automatically: the problem is in your request, not on the server. Fix the parameters first.

401: Unauthorized

The API key is invalid, expired, or missing:

if response.status_code == 401:
    raise AuthenticationError("Invalid or expired API key")

Check that:

  • The Authorization header is present
  • The format is Bearer xai-... (with a space after Bearer)
  • The key hasn’t been revoked in the xAI console

404: Not Found

The voice ID is unknown: check it with GET /v1/tts/voices for a built-in voice, or GET /v1/custom-voices for a custom voice. Like a 400, a 404 won’t go away if you retry.

429: Too Many Requests

You’ve exceeded a rate limit: on the entry tier, 50 requests per second and 100 concurrent sessions for TTS according to the Rate limits page, and 50 concurrent WebSocket sessions per team according to the Text to Speech page. xAI recommends retrying with exponential backoff:

if response.status_code == 429:
    retry_after = int(response.headers.get("Retry-After", 5))
    await asyncio.sleep(retry_after)
    # Try again

Implement exponential backoff for 429 errors:

async def request_with_backoff(url, payload, headers, max_retries=5):
    for attempt in range(max_retries):
        response = requests.post(url, json=payload, headers=headers)

        if response.status_code == 200:
            return response

        if response.status_code == 429:
            wait = min(2 ** attempt + random.random(), 60)
            print(f"Rate limited. Waiting {wait:.1f}s")
            await asyncio.sleep(wait)
            continue

        if response.status_code >= 500:
            wait = min(2 ** attempt + random.random(), 30)
            await asyncio.sleep(wait)
            continue

        # Client error (400, 401): don't retry
        response.raise_for_status()

    raise Exception("Failed after 5 attempts")

500 / 503: Server Error

The server has a temporary problem. Retry with backoff:

if response.status_code in (500, 503):
    # Retry with exponential backoff
    pass

503 errors are often caused by temporary overload. They usually clear up within a few seconds.

Never expose the API key on the client

This is the most important security rule. If your API key is visible in the browser’s JavaScript, anyone can:

  • Use your quota for free
  • Run up charges on your account
  • Access every xAI API endpoint with your key

Backend proxy architecture

The solution is to proxy TTS calls through your own backend:

Browser -> Your backend -> xAI API
 (no key)    (API key)      (TTS)

Express.js example

const express = require("express");
const app = express();

app.post("/api/tts", async (req, res) => {
  const { text, voice_id, language } = req.body;

  // Server-side validation
  if (!text || text.length > 60000) {
    return res.status(400).json({ error: "Invalid text" });
  }

  const response = await fetch("https://api.x.ai/v1/tts", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.XAI_API_KEY}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({ text, voice_id, language })
  });

  if (!response.ok) {
    return res.status(response.status).json({
      error: "TTS error"
    });
  }

  // Pass the audio on to the client
  res.set("Content-Type", response.headers.get("Content-Type"));
  const buffer = await response.arrayBuffer();
  res.send(Buffer.from(buffer));
});

FastAPI example (Python)

from fastapi import FastAPI, HTTPException
from fastapi.responses import Response
import httpx

app = FastAPI()

@app.post("/api/tts")
async def proxy_tts(text: str, voice_id: str = "eve", language: str = "en"):
    if len(text) > 60000:
        raise HTTPException(400, "Text too long")

    async with httpx.AsyncClient() as client:
        response = await client.post(
            "https://api.x.ai/v1/tts",
            headers={"Authorization": f"Bearer {os.environ['XAI_API_KEY']}"},
            json={"text": text, "voice_id": voice_id, "language": language}
        )

    if response.status_code != 200:
        raise HTTPException(response.status_code, "TTS error")

    return Response(
        content=response.content,
        media_type=response.headers["content-type"]
    )

WebSocket proxy

For TTS WebSocket, your backend has to relay messages between the client and the xAI API:

const WebSocket = require("ws");

wss.on("connection", (clientWs) => {
  const xaiWs = new WebSocket(
    "wss://api.x.ai/v1/tts?language=en&voice=eve",
    { headers: { Authorization: `Bearer ${process.env.XAI_API_KEY}` } }
  );

  // Relay client -> xAI
  clientWs.on("message", (msg) => xaiWs.send(msg));

  // Relay xAI -> client
  xaiWs.on("message", (msg) => clientWs.send(msg));

  // Handle closing
  clientWs.on("close", () => xaiWs.close());
  xaiWs.on("close", () => clientWs.close());
});

Fail-open moderation

The TTS API runs content moderation asynchronously. That means moderation doesn’t block the request: the audio is generated and returned even if moderation hasn’t finished its analysis.

If content is flagged as inappropriate, the report comes after the fact. So your app should run its own moderation upstream if needed.

Key takeaways

  • 400 = a parameter error (don’t retry; fix the request)
  • 401 = an invalid API key (check the Authorization header)
  • 429 = rate limited (exponential backoff)
  • 500/503 = a temporary server error (retry with backoff)
  • Never expose the API key in client code
  • Proxy all TTS calls through your backend
  • Moderation is fail-open: add your own filtering if needed