RealtimeVoiceConversation
Browser capture and playback for provider-direct speech-to-speech sessions.
RealtimeVoiceConversation is the browser-side helper for Speko speech-to-speech (S2S) sessions. POST /v1/sessions reserves credit and returns a short-lived provider credential; the browser then connects directly to OpenAI, xAI, or Gemini Live. Realtime audio does not pass through a Speko media proxy.
Use it when you want the lowest-latency S2S path and do not need the browser media transport used by VoiceConversation.
import { RealtimeVoiceConversation } from '@spekoai/client';Mint the session on your server
Create S2S sessions on your backend so SPEKO_API_KEY never reaches the browser. Return the provider-direct response unchanged; it contains only a scoped, short-lived provider credential plus Speko telemetry and billing-control URLs.
app.post('/api/realtime-session', async (req, res) => {
// Preserve this value if your server retries an ambiguous upstream timeout.
const idempotencyKey = req.get('Idempotency-Key') || crypto.randomUUID();
const response = await fetch('https://api.speko.dev/v1/sessions', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SPEKO_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify({
mode: 's2s',
s2s: {
provider: 'openai',
model: 'gpt-realtime-2.1',
voice: 'marin',
systemPrompt: 'You are a concise voice assistant.',
},
ttlSeconds: 900,
}),
});
if (!response.ok) {
res.status(response.status).json({ error: 'Could not start realtime session' });
return;
}
res.json(await response.json());
});Connect from the browser
import { useEffect, useRef, useState } from 'react';
import { RealtimeVoiceConversation } from '@spekoai/client';
export function RealtimePanel() {
const convRef = useRef<RealtimeVoiceConversation | null>(null);
const [status, setStatus] = useState('idle');
const [transcript, setTranscript] = useState<string[]>([]);
async function start() {
setStatus('connecting');
const session = await fetch('/api/realtime-session', {
method: 'POST',
}).then((r) => r.json());
const conv = await RealtimeVoiceConversation.create({
...session,
onConnect: ({ conversationId }) => {
console.log('connected', conversationId);
},
onStatusChange: setStatus,
onMessage: ({ source, text, isFinal }) => {
if (isFinal) setTranscript((items) => [...items, `${source}: ${text}`]);
},
onError: (err) => console.error(err),
onDisconnect: () => setStatus('idle'),
});
convRef.current = conv;
}
async function stop() {
await convRef.current?.endSession();
convRef.current = null;
}
useEffect(() => () => { void convRef.current?.endSession(); }, []);
return (
<div>
<button onClick={start} disabled={status !== 'idle'}>Start</button>
<button onClick={stop} disabled={status === 'idle'}>Stop</button>
<p>Status: {status}</p>
<ul>{transcript.map((item, i) => <li key={i}>{item}</li>)}</ul>
</div>
);
}RealtimeVoiceConversation.create(options)
static create(options: RealtimeConversationOptions): Promise<RealtimeVoiceConversation>create() validates the provider endpoint, connects directly to it, waits for provider readiness, starts microphone capture, then resolves. OpenAI uses WebRTC and a provider-authenticated billing sideband; xAI and Gemini Live use provider WebSockets. The client automatically renews authorized Gemini Live slices with session resumption when the session horizon is longer than one slice.
RealtimeConversationOptions
| Field | Type | Required | Description |
|---|---|---|---|
transport | 'provider_direct' | yes | Ensures no Speko media-proxy transport is accepted. |
sessionId | string | yes | Server-assigned session id. Also returned by getId(). |
attemptId | string | yes | Correlates the provider connection with billing evidence. |
provider | 'openai' | 'xai' | 'google' | yes | Selected realtime provider. |
providerTransport | 'webrtc' | 'websocket' | yes | OpenAI plans use WebRTC; xAI and Gemini Live use WebSocket. |
endpoint | string | yes | Allowlisted provider endpoint. |
credential | object | yes | Scoped provider bearer credential and expiry. |
telemetry | object | yes | Speko control endpoint and short-lived token; it never carries media. |
reservation | object | yes | Authorized duration, lease expiry, billing ceiling, and optional renewal. |
sidebandUrl | string? | OpenAI | Binds the OpenAI call to the provider-authenticated metering sideband. |
inputSampleRate | 16000 | 24000? | Gemini uses 16 kHz input; OpenAI and xAI use 24 kHz. | |
outputSampleRate | 24000? | Provider response PCM sample rate. | |
inputDeviceId | string? | Specific microphone deviceId. | |
audioConstraints | AudioConstraints? | echoCancellation, noiseSuppression, and autoGainControl. | |
onConnect | (d: { conversationId }) => void | Fired after provider readiness and microphone capture. | |
onDisconnect | (d: DisconnectionDetails) => void | Fired when the provider transport closes. | |
onMessage | (m: ConversationMessage) => void | Provider transcripts mapped to { source, text, isFinal }. | |
onStatusChange | (s: ConversationStatus) => void | connecting, connected, disconnecting, or disconnected. | |
onModeChange | (m: ConversationMode) => void | speaking while response audio is queued, otherwise listening. | |
onError | (err: Error) => void | Provider, sideband, or entitlement-renewal errors. |
Instance methods
getId(): string
Returns the sessionId passed to create().
isOpen(): boolean
true while the SDK status is connected and the provider WebSocket or OpenAI data channel is open.
setMicMuted(muted: boolean): Promise<void>
Mute or unmute local microphone capture. Muting disables the media track and stops PCM frames from being sent.
setVolume(volume: number): void
Set response playback volume from 0 to 1. Values outside that range are clamped.
endSession(): Promise<void>
Close the provider transport, stop microphone tracks, clear queued playback, close audio contexts, and transition to disconnected.
Transport notes
- OpenAI audio travels over WebRTC. The Speko sideband receives provider events and can terminate the provider call, but it never receives audio.
- xAI and Gemini audio travels over provider WebSockets using only delegated credentials.
- Gemini Live renewals reserve the next credit slice before a new credential is minted, then resume the provider session on the rotated connection.
- Client telemetry is operational evidence only. Missing or altered telemetry cannot reduce the authorized entitlement settlement.
AudioWorkletcapture is used when available; the SDK falls back toScriptProcessorNodefor older browsers.