Ephemeral tokens and security
On this page
Updated November 3, 2026, based on the xAI Voice API reference. Sources: Ephemeral Tokens · Voice API reference · Session Resumption.
The problem with an API key on the client
A voice app running in a browser or on a phone can’t ship your API key. Any visitor can open the developer tools, read the key from the JavaScript bundle or a request header, and then use it on your account until your quota runs out. No amount of obfuscation protects a value the client must have in order to call the API.
xAI’s answer is ephemeral tokens, also called client secrets: a short-lived token, generated by your server, that authorizes a voice session and nothing else.
How it works
The flow comes down to five steps:
- Your backend (a secure server) calls
POST /v1/realtime/client_secretswith your API key - The API returns a temporary token with a limited lifetime
- Your backend sends that token to the client (browser or mobile app)
- The client uses the token to connect to the Voice Agent WebSocket
- The token expires after the lifetime you chose (
expires_after.seconds)
What can leak on the client side is now worth much less: a token that expires within minutes and can only open a voice session, not your API key. On the server, generating one takes a few lines of code. Notice that the handler starts by checking the user: without that check, your backend is just a public token dispenser.
// Node.js backend (Express)
app.post("/api/voice-token", async (req, res) => {
// Check that the user is authenticated
if (!req.user) return res.status(401).json({ error: "Unauthorized" });
const response = await fetch(
"https://api.x.ai/v1/realtime/client_secrets",
{
method: "POST",
headers: {
"Authorization": "Bearer " + process.env.XAI_API_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({
expires_after: { seconds: 600 } // 10 minutes, the default
})
}
);
const data = await response.json(); // { value, expires_at }
res.json({ token: data.value, expiresAt: data.expires_at });
});
The client only knows your own route: it asks for a token, then opens the WebSocket with it.
// Frontend (browser)
async function startVoiceSession() {
// Ask the backend for a token
const tokenRes = await fetch("/api/voice-token", { method: "POST" });
const { token } = await tokenRes.json();
// The browser WebSocket doesn't accept headers:
// the token goes in a subprotocol, prefixed with "xai-client-secret."
const ws = new WebSocket(
"wss://api.x.ai/v1/realtime?model=grok-voice-latest",
[`xai-client-secret.${token}`]
);
// ... session configuration
}
The end-to-end architecture
Put together, the flow starts with the user signing in to your app. Their client then asks your backend for a voice token, and the backend checks authentication and permissions before generating the ephemeral token through the xAI API. The token travels back to the client, which uses it to open the Voice Agent session, and it expires automatically at the end of its lifetime.
That check between the request and the generation turns a simple relay into a checkpoint, and that’s where your business rules live. You can cap the number of voice sessions per user per day, so a single account can’t burn through your monthly budget overnight. You can reserve tokens for active subscriptions, enforcing your business model somewhere other than the interface. You can rate-limit requests, so a buggy or malicious client can’t ask for a thousand tokens a minute. And you can log every token you generate: it’s your only reliable basis for monitoring, internal billing, and incident analysis.
Rules you must never break
The API key never goes to the client, in any form. Keep it out of front-end JavaScript and out of any environment variable exposed to the browser build (variables prefixed to be injected into the bundle end up public). Keep it out of URLs, logs, and error messages shown to users, too.
Next, keep the lifetime as short as is practical. According to the API reference, expires_after.seconds defaults to 600 seconds and maxes out at 3,600; the documentation uses 300 in its examples. Above all, the token needs to be valid when the client opens the connection. A short lifetime shrinks the window during which an intercepted token can be used.
The documentation doesn’t say whether an expiring token cuts off a connection that’s already open. If your client needs to reconnect, after a network drop or during a very long session, request a new token and use session resumption: enable resumption.enabled in session.update, keep the ID you receive in conversation.created, then reopen the WebSocket with ?conversation_id=<id>. The previous turns are replayed, and the model picks up where it left off.
async function reconnect(conversationId) {
const { token } = await (await fetch("/api/voice-token", { method: "POST" })).json();
const url = `wss://api.x.ai/v1/realtime?model=grok-voice-latest&conversation_id=${conversationId}`;
const ws = new WebSocket(url, [`xai-client-secret.${token}`]);
ws.onopen = () => ws.send(JSON.stringify({
type: "session.update",
session: { resumption: { enabled: true } }, // send again on every connection
}));
return ws;
}
For TTS in HTTP mode, there’s an even simpler alternative to tokens: a proxy. The client sends its text to your backend, and your backend calls TTS and returns the audio. The key never leaves your server, and you get a natural place to filter content and meter usage along the way.
Key takeaways
- Ephemeral tokens secure access to the Voice Agent from client-side apps
POST /v1/realtime/client_secretsreturnsvalueandexpires_at;expires_after.secondsdefaults to 600 and maxes out at 3,600- In the browser, the token goes in the
xai-client-secret.<token>subprotocol - Your backend must check authentication and permissions before generating a token
- Never expose your API key on the client, in URLs, or in logs
- For HTTP TTS, proxying through your backend is a valid alternative to ephemeral tokens