Speech to Text Streaming API
The Streaming API receives audio chunks over WebSocket and returns transcript deltas and completion events. Event names follow an OpenAI Realtime transcription-compatible shape, while the connection path is specific to Kitsch Labs.
Connection
wss://api.kitschlabs.com/v1/speech-to-text/stream
Include one of the following authentication headers in the WebSocket handshake from your server environment.
Authorization: Bearer <API_KEY>
You can also use xi-api-key. The standard browser WebSocket API cannot set
arbitrary authentication headers. Keep the API key on your server and connect
from there.
After authentication, the server sends session.created first.
Session configuration
Send one session.update before sending audio.
{
"type": "session.update",
"session": {
"type": "transcription",
"audio": {
"input": {
"format": {"type": "audio/pcm", "rate": 24000},
"transcription": {
"model": "kitsch-stt-v1",
"language": "en",
"prompt": "Kitsch Labs"
},
"turn_detection": {"type": "server_vad"}
}
}
}
}
The server returns the applied settings in session.updated. The session
cannot be changed after the first audio chunk.
Supported audio formats
| Format | Input requirements |
|---|---|
audio/pcm | 24 kHz, mono, signed 16-bit little-endian PCM |
audio/pcmu | 8 kHz, mono, G.711 μ-law |
audio/pcma | 8 kHz, mono, G.711 A-law |
The default is audio/pcm at 24 kHz.
How do I send audio?
Base64-encode each chunk. One decoded chunk can be up to 256 KiB.
{"type":"input_audio_buffer.append","audio":"BASE64_AUDIO"}
The default server_vad automatically detects the end of an utterance. For
manual turns, set turn_detection to null, then send:
{"type":"input_audio_buffer.commit"}
What events will I receive?
| Event | Description |
|---|---|
session.created | Transcription session created |
session.updated | Session settings applied |
input_audio_buffer.committed | One utterance has been committed |
conversation.item.input_audio_transcription.delta | Finalized transcript fragment |
conversation.item.input_audio_transcription.completed | Complete transcript and usage for the utterance |
error | Request or processing error |
Delta event:
{
"type": "conversation.item.input_audio_transcription.delta",
"event_id": "event_...",
"item_id": "item_...",
"content_index": 0,
"delta": "Hello"
}
Completion event:
{
"type": "conversation.item.input_audio_transcription.completed",
"event_id": "event_...",
"item_id": "item_...",
"content_index": 0,
"transcript": "Hello.",
"usage": {"type": "duration", "seconds": 1.25}
}
Append deltas in order by item_id and content_index. Use the completed
event's transcript as the final result for that utterance.
Connection example
This example sends a 24 kHz mono signed 16-bit little-endian raw PCM file.
import asyncio
import base64
import json
import os
from websockets.asyncio.client import connect
async def transcribe() -> None:
headers = {"Authorization": f"Bearer {os.environ['KITSCH_API_KEY']}"}
async with connect(
"wss://api.kitschlabs.com/v1/speech-to-text/stream",
additional_headers=headers,
) as websocket:
print(await websocket.recv()) # session.created
await websocket.send(json.dumps({
"type": "session.update",
"session": {
"type": "transcription",
"audio": {
"input": {
"format": {"type": "audio/pcm", "rate": 24000},
"transcription": {"model": "kitsch-stt-v1", "language": "en"},
"turn_detection": None,
}
},
},
}))
with open("speech.pcm", "rb") as audio:
while chunk := audio.read(64 * 1024):
await websocket.send(json.dumps({
"type": "input_audio_buffer.append",
"audio": base64.b64encode(chunk).decode("ascii"),
}))
await websocket.send(json.dumps({"type": "input_audio_buffer.commit"}))
async for raw_event in websocket:
event = json.loads(raw_event)
if event["type"] == "conversation.item.input_audio_transcription.delta":
print(event["delta"], end="", flush=True)
elif event["type"] == "conversation.item.input_audio_transcription.completed":
print()
break
elif event["type"] == "error":
raise RuntimeError(event["error"]["message"])
asyncio.run(transcribe())
Limits and billing
- Each connection has a maximum session length of 30 seconds.
- Connection time is rounded up to the next started second and billed at 0.1 credits per second.
- One connection can process multiple utterances, but open a new session before the 30-second limit.
- After a temporary connection error, reconnect with a new session. Do not automatically replay audio from the previous session.
Error handling
Unsupported events and invalid settings can return an error event while the
connection remains open.
{
"type": "error",
"event_id": "event_...",
"error": {
"type": "invalid_request_error",
"code": "invalid_audio",
"message": "The audio field must be valid base64.",
"param": null,
"event_id": "<REQUEST_ID>"
}
}
| Close code | Meaning | Recommended action |
|---|---|---|
1000 | Normal completion or session limit | Open a new session if needed |
1008 | Authentication, permission, or request policy error | Check the key, credits, and settings |
1011 | Temporary processing error | Reconnect selectively |
1013 | Concurrent processing capacity reached | Apply backoff before reconnecting |