Grok Voice 3.0 is xAI’s speech-to-speech model for voice agents, released on 14 October 2026. You stream speech in over one WebSocket and it answers with speech: 28 built-in voices, 20+ languages with automatic detection (Chinese included), and function calling. $0.104 per minute of session, billed per second.
Grok Voice 3.0 guide: how to use it, pricing, voices, vs GPT RealtimeTry a live conversation in the browser
wss://api.apimodels.app/v1/realtime?model=grok-voice-3.0Send your apimodels.app API key as a header when you connect:
Authorization: Bearer YOUR_API_KEYBrowsers cannot set WebSocket headers. Your server mints a short-lived token and the browser passes it as a subprotocol. Never put the API key in the browser.
curl -X POST https://api.apimodels.app/v1/realtime/client_secrets \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"expires_after": {"seconds": 60}}'
# { "value": "<token>", "expires_at": 1792130400 }| Field | Required | Type | Description |
|---|---|---|---|
| expires_after.seconds | No | number | Token lifetime, 10 to 600 seconds, default 60. It only governs when the token can be used to connect. |
Returns { "value": "<token>", "expires_at": <unix seconds> }.
new WebSocket("wss://api.apimodels.app/v1/realtime?model=grok-voice-3.0", ["xai-client-secret." + value])Send a session.update right after connecting. Every field is optional and you can send it again later.
{
"type": "session.update",
"session": {
"voice": "eve",
"instructions": "You are Grok Voice 3.0, a realtime voice assistant built by xAI. You are a friendly support agent. Keep answers short.",
"turn_detection": { "type": "server_vad" },
"audio": {
"input": { "format": { "type": "audio/pcm", "rate": 24000 },
"transcription": { "language_hint": "zh-CN" } },
"output": { "format": { "type": "audio/pcm", "rate": 24000 }, "speed": 1.0 }
},
"tools": [
{
"type": "function",
"name": "get_order_status",
"description": "Look up an order by its id",
"parameters": {
"type": "object",
"properties": { "order_id": { "type": "string" } },
"required": ["order_id"]
}
}
]
}
}| Field | Required | Type | Description |
|---|---|---|---|
| voice | No | string | One of the 28 built-in voices, default eve. See the voice table below. |
| instructions | No | string | System prompt: persona, tone and task. |
| turn_detection | No | object | null | {"type":"server_vad"} lets the server detect when you stop talking; null means manual turns (commit + response.create). |
| audio.input.format | No | object | Default {"type":"audio/pcm","rate":24000}; audio/pcmu, audio/pcma and audio/opus are also accepted. |
| audio.output.format | No | object | Same options, for the audio the model sends back. |
| audio.output.speed | No | number | Speaking rate, 0.7 to 1.5. |
| audio.input.transcription.language_hint | No | string | A BCP-47 tag such as zh-CN or en-US. Set it when you know the caller’s language. |
| tools | No | array | Function tools ({"type":"function", name, description, parameters}). Only function tools are available on apimodels.app for now. |
Server-side tools (web_search, x_search, file_search, mcp) are not available yet: a session.update that includes them gets an error event. Function calling itself has no extra fee.
Default format: 16-bit PCM, 24 kHz, mono, little-endian, base64-encoded inside the JSON event.
// with turn_detection {"type":"server_vad"}: just keep appending,
// the server decides when you stopped talking and answers.
{ "type": "input_audio_buffer.append", "audio": "<base64 16-bit PCM, 24 kHz, mono>" }
// with turn_detection null (manual turns): append, then commit, then ask for a reply
{ "type": "input_audio_buffer.append", "audio": "<base64 ...>" }
{ "type": "input_audio_buffer.commit" }
{ "type": "response.create" }
// throw away audio you have not committed yet
{ "type": "input_audio_buffer.clear" }You can also talk to it without a microphone: add a text message and ask for a reply. Each input_text message costs $0.0052.
{
"type": "conversation.item.create",
"item": {
"type": "message",
"role": "user",
"content": [{ "type": "input_text", "text": "What can you do?" }]
}
}
{ "type": "response.create" }// server -> you
{ "type": "response.function_call_arguments.done",
"call_id": "call_123", "name": "get_order_status", "arguments": "{\"order_id\":\"A-1001\"}" }
// you -> server: the result, then ask the model to continue speaking
{ "type": "conversation.item.create",
"item": { "type": "function_call_output", "call_id": "call_123", "output": "{\"status\":\"shipped\"}" } }
{ "type": "response.create" }# pip install websockets (websockets 14+; on older versions pass extra_headers= instead)
import asyncio, base64, json, os, wave
import websockets
URL = "wss://api.apimodels.app/v1/realtime?model=grok-voice-3.0"
API_KEY = os.environ["APIMODELS_API_KEY"]
async def main():
headers = {"Authorization": f"Bearer {API_KEY}"}
pcm = bytearray()
asked = False
async with websockets.connect(URL, additional_headers=headers) as ws:
# 1. Configure the session. turn_detection None = manual turns (no microphone here).
await ws.send(json.dumps({
"type": "session.update",
"session": {
"voice": "eve",
"instructions": "You are Grok Voice 3.0, a realtime voice assistant built by xAI. Keep answers short.",
"turn_detection": None,
"audio": {"output": {"format": {"type": "audio/pcm", "rate": 24000}}},
},
}))
async for raw in ws:
event = json.loads(raw)
kind = event["type"]
if kind == "session.updated" and not asked:
asked = True
# 2. One text message ($0.0052), then ask for a spoken reply.
await ws.send(json.dumps({
"type": "conversation.item.create",
"item": {
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "Introduce yourself in one sentence."}],
},
}))
await ws.send(json.dumps({"type": "response.create"}))
elif kind == "response.output_audio.delta":
pcm += base64.b64decode(event["delta"])
elif kind == "response.output_audio_transcript.delta":
print(event["delta"], end="", flush=True)
elif kind == "error":
raise RuntimeError(event)
elif kind == "response.done":
break
# 3. The audio is 16-bit PCM, 24 kHz, mono: wrap it in a WAV header.
with wave.open("reply.wav", "wb") as wav:
wav.setnchannels(1)
wav.setsampwidth(2)
wav.setframerate(24000)
wav.writeframes(bytes(pcm))
print()
print("saved reply.wav")
asyncio.run(main())// npm install ws (Node 18+, save as reply.mjs)
import fs from "node:fs";
import WebSocket from "ws";
const ws = new WebSocket("wss://api.apimodels.app/v1/realtime?model=grok-voice-3.0", {
headers: { Authorization: "Bearer " + process.env.APIMODELS_API_KEY },
});
const chunks = [];
let asked = false;
ws.on("open", () => {
ws.send(JSON.stringify({
type: "session.update",
session: {
voice: "eve",
instructions: "You are Grok Voice 3.0, a realtime voice assistant built by xAI. Keep answers short.",
turn_detection: null,
},
}));
});
ws.on("message", (data) => {
const event = JSON.parse(data.toString());
if (event.type === "session.updated" && !asked) {
asked = true;
ws.send(JSON.stringify({
type: "conversation.item.create",
item: {
type: "message",
role: "user",
content: [{ type: "input_text", text: "Introduce yourself in one sentence." }],
},
}));
ws.send(JSON.stringify({ type: "response.create" }));
} else if (event.type === "response.output_audio.delta") {
chunks.push(Buffer.from(event.delta, "base64"));
} else if (event.type === "error") {
console.error(event);
} else if (event.type === "response.done") {
// Raw 16-bit PCM, 24 kHz, mono. To get a WAV:
// ffmpeg -f s16le -ar 24000 -ac 1 -i reply.pcm reply.wav
fs.writeFileSync("reply.pcm", Buffer.concat(chunks));
ws.close();
}
});// ── On your server (Node 18+, any framework). The API key never leaves the server. ──
export async function mintRealtimeToken() {
const res = await fetch("https://api.apimodels.app/v1/realtime/client_secrets", {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.APIMODELS_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ expires_after: { seconds: 60 } }), // 10 to 600, default 60
});
return res.json(); // { value, expires_at }
}
// Expose it to your own logged-in users, e.g. POST /api/realtime-token -> mintRealtimeToken()// ── In the browser. Call start() from a click: audio only starts after a user gesture. ──
async function start() {
const { value } = await (await fetch("/api/realtime-token", { method: "POST" })).json();
const ws = new WebSocket(
"wss://api.apimodels.app/v1/realtime?model=grok-voice-3.0",
["xai-client-secret." + value],
);
const ctx = new AudioContext({ sampleRate: 24000 });
let playAt = 0;
ws.onopen = async () => {
ws.send(JSON.stringify({
type: "session.update",
session: {
voice: "eve",
instructions: "You are Grok Voice 3.0, a realtime voice assistant built by xAI.",
turn_detection: { type: "server_vad" },
},
}));
// Microphone -> 16-bit PCM -> input_audio_buffer.append.
// ScriptProcessorNode keeps the example short; use an AudioWorklet in production.
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const mic = ctx.createMediaStreamSource(stream);
const proc = ctx.createScriptProcessor(4096, 1, 1);
proc.onaudioprocess = (e) => {
const f32 = e.inputBuffer.getChannelData(0);
const pcm = new Int16Array(f32.length);
for (let i = 0; i < f32.length; i++) {
const s = Math.max(-1, Math.min(1, f32[i]));
pcm[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
}
const bytes = new Uint8Array(pcm.buffer);
let bin = "";
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: "input_audio_buffer.append", audio: btoa(bin) }));
}
};
mic.connect(proc);
proc.connect(ctx.destination);
};
// Model audio -> speakers, queued back to back.
ws.onmessage = (e) => {
const event = JSON.parse(e.data);
if (event.type === "error") console.error(event);
if (event.type !== "response.output_audio.delta") return;
const bin = atob(event.delta);
const buf = ctx.createBuffer(1, bin.length / 2, 24000);
const out = buf.getChannelData(0);
for (let i = 0; i < out.length; i++) {
const v = bin.charCodeAt(2 * i) | (bin.charCodeAt(2 * i + 1) << 8);
out[i] = (v >= 0x8000 ? v - 0x10000 : v) / 32768;
}
const src = ctx.createBufferSource();
src.buffer = buf;
src.connect(ctx.destination);
playAt = Math.max(playAt, ctx.currentTime);
src.start(playAt);
playAt += buf.duration;
};
}| Event | What it does |
|---|---|
| session.update | Set the voice, instructions, turn detection, audio formats and function tools. Can be sent again at any time. |
| conversation.item.create | Add a text message (input_text, $0.0052 each) or return a function result (function_call_output). |
| response.create | Ask the model to respond. Needed in manual-turn mode and after a text message or function result. |
| input_audio_buffer.append | Send a chunk of base64 audio (field audio). |
| input_audio_buffer.commit | End the user turn in manual mode (turn_detection null). |
| input_audio_buffer.clear | Discard audio that has not been committed. |
| Event | What it does |
|---|---|
| session.created | The connection is ready; carries the current session config. |
| session.updated | Your session.update has been applied. |
| conversation.created | The conversation has been created. |
| conversation.item.added | A message or function call was added to the conversation. |
| conversation.item.input_audio_transcription.updated | Transcript of the user’s speech, cumulative: each event carries the full text so far, not a delta. |
| response.created | The model has started a response. |
| response.output_audio.delta | A chunk of base64 audio, in the delta field. |
| response.output_audio_transcript.delta | Text of what the model is saying, incremental. |
| response.output_audio.done | All audio for this response has been sent. |
| response.function_call_arguments.done | The model wants a function called: carries call_id, name and arguments as a JSON string. |
| response.done | The response is finished. |
| error | Something went wrong. When the balance runs out the error type is insufficient_balance and the session then closes. |
28 built-in voices, and every one speaks every supported language, so there is no separate voice per language. Set it with voice in session.update; the default is eve.
| Type | voice |
|---|---|
| Female (9) | ara, aurora, carina, celeste, eve (default), iris, liora, luna, ursa |
| Male (19) | altair, atlas, castor, cosmo, helios, helix, kepler, leo, lumen, lux, naksh, orion, perseus, rex, rigel, sal, sirius, zagan, zenith |
Languages: 20+, with native accents and automatic detection, including Simplified Chinese.
| Item | Price |
|---|---|
| Connected session time | $0.104 per minute, billed per second |
| Text message (input_text via conversation.item.create) | $0.0052 each |
| Function calling | No extra fee |
| Example: a 90-second session | $0.156 |
The event model is largely the same: session.update, conversation.item.create, response.create, input_audio_buffer.* and response.output_audio.delta behave alike, so an OpenAI Realtime client ports with small changes. Watch for:
Want to hear it first? Try it in the browser on the model page (billed at the same $0.104 per minute).
Open a WebSocket to wss://api.apimodels.app/v1/realtime?model=grok-voice-3.0. From a server, send Authorization: Bearer YOUR_API_KEY. From a browser, have your server call POST https://api.apimodels.app/v1/realtime/client_secrets (token lifetime 10 to 600 seconds, default 60) and connect with the subprotocol xai-client-secret.<value>. The endpoint is only on api.apimodels.app, not under apimodels.app/api/v1.
$0.104 per minute of connected session, billed per second, so a 90-second session is $0.156. Each input_text message sent with conversation.item.create costs $0.0052; function calling is free. You need one minute of balance to connect, each minute is charged at its start, unused seconds are refunded at the end, and a failed connection is not charged.
Sessions last up to 60 minutes, each account can run 5 at once, and a session with no client events for 10 minutes is closed. Audio defaults to 16-bit PCM at 24 kHz, mono, base64 in JSON; audio/pcmu, audio/pcma and audio/opus are also accepted. 28 built-in voices (default eve) each speak all 20+ supported languages.