HTTP errors and API key security
On this page
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
Authorizationheader 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