Speko Docs

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

FieldTypeRequiredDescription
transport'provider_direct'yesEnsures no Speko media-proxy transport is accepted.
sessionIdstringyesServer-assigned session id. Also returned by getId().
attemptIdstringyesCorrelates the provider connection with billing evidence.
provider'openai' | 'xai' | 'google'yesSelected realtime provider.
providerTransport'webrtc' | 'websocket'yesOpenAI plans use WebRTC; xAI and Gemini Live use WebSocket.
endpointstringyesAllowlisted provider endpoint.
credentialobjectyesScoped provider bearer credential and expiry.
telemetryobjectyesSpeko control endpoint and short-lived token; it never carries media.
reservationobjectyesAuthorized duration, lease expiry, billing ceiling, and optional renewal.
sidebandUrlstring?OpenAIBinds the OpenAI call to the provider-authenticated metering sideband.
inputSampleRate16000 | 24000?Gemini uses 16 kHz input; OpenAI and xAI use 24 kHz.
outputSampleRate24000?Provider response PCM sample rate.
inputDeviceIdstring?Specific microphone deviceId.
audioConstraintsAudioConstraints?echoCancellation, noiseSuppression, and autoGainControl.
onConnect(d: { conversationId }) => voidFired after provider readiness and microphone capture.
onDisconnect(d: DisconnectionDetails) => voidFired when the provider transport closes.
onMessage(m: ConversationMessage) => voidProvider transcripts mapped to { source, text, isFinal }.
onStatusChange(s: ConversationStatus) => voidconnecting, connected, disconnecting, or disconnected.
onModeChange(m: ConversationMode) => voidspeaking while response audio is queued, otherwise listening.
onError(err: Error) => voidProvider, 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.
  • AudioWorklet capture is used when available; the SDK falls back to ScriptProcessorNode for older browsers.

On this page