Realtime Translation
All plansOne audio stream. Live captions and translations in the languages you choose.
Quick start
-
1
Connect with your API key
Create a key in Settings, then open this WebSocket. Replace
YOUR_API_KEYwith your key.wss://extapi.bizcrush.ai/v1/translate/stream?api_key=YOUR_API_KEY -
2
Choose your languages
Send this JSON as the first text frame. This example translates English speech into Spanish. Omit
language_hintsfor automatic input-language detection.Choose 1–10 output languages in
target_languages.{ "target_languages": ["es"], "language_hints": ["en"], "enable_diarization": true } -
3
Wait for the connection, then send audio
After
{"connected":true}, send audio as binary frames at the speed it is spoken.PCM16 (default) or Opus
PCM16: 16 kHz, mono, little-endian; maximum 32,000 bytes per frame.
Sending Opus audio
Use this as your first config for browser WebM/Opus. Send one audio-only recording, including its initial headers. Blocks may span multiple WebSocket messages.
{ "encoding": "opus", "container": "webm", "target_languages": ["es"], "language_hints": ["en"] }For raw Opus, use
"container":"raw"and send one complete packet per binary message. Use"container":"ogg"for Ogg/Opus. Mono and stereo are accepted and converted to 16 kHz mono. WebM must contain exactly one Opus audio track.Send 20–100 ms chunks at realtime speed. Each message may contain at most 64 KiB and one second of decoded audio. Omit
containerto auto-detect Ogg or WebM headers. Keep glossary and other options in the same config. -
4
Read the captions and finish the stream
You receive the original text, translations, and speaker labels. Update each caption by its
id; translations can arrive after the original text is final.When audio ends, send an empty binary frame or
{"type":"finish"}. Keep reading until{"finished":true}, then let the server close.
Browser microphone: WebM/Opus
Keep every dataavailable chunk in order, including the last chunk after stop(). Do not restart the recorder or resend its header on the same WebSocket.
const mimeType = "audio/webm;codecs=opus";
if (!MediaRecorder.isTypeSupported(mimeType)) throw new Error("Use PCM16 on this browser");
const stream = await navigator.mediaDevices.getUserMedia({audio: true});
const recorder = new MediaRecorder(stream, {mimeType});
const ws = new WebSocket("wss://extapi.bizcrush.ai/v1/translate/stream?api_key=YOUR_API_KEY");
let pending = Promise.resolve();
recorder.ondataavailable = ({data}) => {
if (data.size) pending = pending.then(async () => {
const bytes = await data.arrayBuffer();
if (ws.readyState === WebSocket.OPEN) ws.send(bytes);
});
};
recorder.onstop = async () => {
await pending;
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({type: "finish"}));
stream.getTracks().forEach(track => track.stop());
};
ws.onopen = () => ws.send(JSON.stringify({"encoding": "opus", "container": "webm", "language_hints": ["en"], "target_languages": ["es"]}));
ws.onmessage = ({data}) => {
const event = JSON.parse(data);
if (event.connected) recorder.start(100);
console.log(event);
};
ws.onclose = () => {
if (recorder.state !== "inactive") recorder.stop();
stream.getTracks().forEach(track => track.stop());
};
// Call recorder.stop() when the user finishes.Custom translations & synonyms
Help recognition with important names, and choose how your terms should appear in each translated language.
Recognition hints
List names and domain terms in context_keywords to help the recognizer.
Preferred translations
Map a source phrase to your preferred wording with translation_terms, separately for each output language.
To give synonyms or spelling variants the same translation, map each source phrase to the same target:
customer support/customer serviceatención al cliente(es)source- Source phrase, synonym, or spelling variant
target- Preferred wording in the translation
target_language- Output language code for this mapping
For custom translations, use this config as the first frame in step 2. Send one config per connection.
{
"target_languages": ["es"],
"language_hints": ["en"],
"translation_terms": [
{"source": "customer support", "target": "atención al cliente", "target_language": "es"},
{"source": "customer service", "target": "atención al cliente", "target_language": "es"}
]
}
See a glossary with multiple output languages
Send this complete config once, before audio. Each entry needs source, target, and an output target_language. The language may be omitted when there is only one output. Mappings guide model wording; source captions retain the recognized text and exact translations can vary with context.
{
"target_languages": [
"es",
"fr"
],
"language_hints": [
"en"
],
"context_keywords": [
"customer support",
"customer service"
],
"translation_terms": [
{
"source": "customer support",
"target": "atención al cliente",
"target_language": "es"
},
{
"source": "customer service",
"target": "atención al cliente",
"target_language": "es"
},
{
"source": "customer support",
"target": "service client",
"target_language": "fr"
},
{
"source": "customer service",
"target": "service client",
"target_language": "fr"
}
],
"enable_diarization": true
}
Response format & late translation updates
Merge translation_update.translations into the caption with the same id, by language. Keep previously received languages when applying a patch. Treat missing or null translations as an empty map. Events are connected, chunk, translation_update, pong, finished, or error.
{"chunk":{"id":"caption-1","text":"Hello, how can I help you?","language":"en","speaker":"0","is_final":true,"translations":{}}}
{"translation_update":{"id":"caption-1","translations":{"es":"Hola, ¿en qué puedo ayudarte?"}}}
Other authentication methods
Server clients can use X-API-Key. Firebase ID tokens are accepted as Authorization: Bearer FIREBASE_ID_TOKEN or ?authorization=FIREBASE_ID_TOKEN. These speech APIs use Firebase ID tokens; MCP uses OAuth access tokens. Authentication failures return HTTP 401; an unavailable backend returns HTTP 503.
Limits & token usage
| Limit | Value |
|---|---|
| Output languages | 1–10 per connection |
| Input-language hints | Up to 3 |
| Keywords / glossary entries | Up to 100 each |
| Combined context | 5,900 characters, including normalized text and separators |
| Audio frame | Up to 32,000 bytes |
| Session length | Up to 55 minutes |
Send the first audio within 30 seconds; pings do not extend this deadline. During silence, send {"type":"ping"} at least every 30 seconds. Audio may run at most one second ahead of elapsed session time.
Minimum charge: one minute. For N output languages, the first valid audio prepays 100 + 60 × N tokens. After the first 60 seconds, each started 30-second period prepays 50 + 30 × N tokens.
Custom context can add a one-time startup charge: each output includes 100 UTF-8 bytes; each started 20 bytes above that costs 1 token. The full reference explains exactly how keywords and glossary bytes are counted.
Payment must succeed before translation starts. Insufficient tokens or payment failures stop the stream. No audio means no charge or provider session.
At capacity, new connections return HTTP 503 with Retry-After. Reconnect with exponential backoff and jitter. Existing streams keep their capacity; a stalled upstream ends with an error. Always wait for finished=true to confirm completion.
Available on every plan. Usage is token-based, with a one-minute minimum. Meetings and transcripts are not stored automatically.
Building with an agent? Full agent reference · OpenAPI JSON
Get Your API Key
Create up to 5 API keys from your account settings for speech and text translation.
Speech-to-Text API
FeaturedSpeech-to-Text API for audio transcription with speaker diarization. Supports file-based (batch) and real-time (streaming) transcription.
Build Your Own STT Service
Use the BizCrush STT API to build your own speech-to-text service. Integrate real-time transcription into your apps, process audio files at scale, or create custom voice-powered workflows — all with speaker diarization and multi-language support.
Automatic language detection across 60 languages — no configuration required. Just send audio and get results.
Overview
Authentication
STT, TTS, and realtime translation require authentication. Send your API key in the X-API-Key header or use this query parameter, including for browser WebSockets:
?api_key=YOUR_API_KEY
You can issue your API key at bizcru.sh/settings.
Firebase ID tokens are also accepted as Authorization: Bearer FIREBASE_ID_TOKEN or ?authorization=FIREBASE_ID_TOKEN. These speech endpoints use Firebase ID tokens; MCP uses OAuth access tokens.
For agents and code generators: full API reference · OpenAPI JSON.
Live STT (WebSocket)
Real-time audio streaming transcription via WebSocket. Send audio chunks and receive interim/final results in real time.
wss://extapi.bizcrush.ai/v1/stt/stream
Query Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| api_key | string | one option | API key, required when no authentication header or Firebase ID token is provided. |
| format | string | optional | "json" for JSON text frames, omit for protobuf binary |
| language_hints | string[] | optional | Language codes as comma-separated (?language_hints=ko,en) or repeated (?language_hints=ko&language_hints=en). Up to 3; extras dropped. Omit for auto-detection. |
| enable_diarization | string | optional | Speaker diarization toggle. "false" to disable; enabled by default. When enabled, each chunk carries a speaker label. |
Protocol
Use ?format=json for the JSON protocol and examples below. Send a JSON text frame with encoding and optional session_id. If format is omitted, send protobuf SessionStreamTranscribeRequest binary messages for config, every audio chunk, and the empty end-of-audio marker; responses are SessionStreamTranscribeResponse.
| Field | Type | Description |
|---|---|---|
| encoding | string | "pcm16" or "opus" (default: "pcm16") |
| session_id | string | Session identifier (optional, auto-generated if omitted) |
| language_hints | string[] | Optional language codes, e.g. ["ko","en"] or "ko,en". Up to 3 entries; extras dropped. Overrides query param. Omit for auto-detection. |
| enable_diarization | boolean | Speaker diarization toggle (default: true). Overrides the query param. |
| context_keywords | string[] | Domain keywords/terms to bias recognition toward names, jargon, etc., e.g. ["Albert Gee","BizCrush"] or "Albert Gee,BizCrush". Up to 2000 characters total; extras dropped. |
{"encoding": "pcm16", "session_id": "optional-session-id"}
Server responds with connection status
{"connected": true}
Send raw audio as binary WebSocket frames
| Encoding | Format | Chunk Size |
|---|---|---|
| pcm16 | 16kHz, mono, 16-bit little-endian | 640 bytes (20ms) recommended |
| opus | Raw Opus (container: "raw"), Ogg/Opus or WebM/Opus |
Opus frame unit |
Server sends interim and final results as JSON text frames
| Field | Type | Description |
|---|---|---|
| chunk.id | string | Chunk ID — same across interim/final for one utterance, new ID after final |
| chunk.session_id | string | Session ID |
| chunk.text | string | Transcribed text (cumulative — each interim contains full text so far) |
| chunk.is_final | boolean | false: interim result, true: final confirmed result |
| chunk.speaker | string | Speaker label ("0", "1", "2", ...) when diarization is enabled. Omitted when disabled or the speaker is unresolved. |
{"chunk": {"id": "01KMCHP57H31XYZABC", "session_id": "my-session", "text": "Hello, how are you?", "is_final": true, "speaker": "1"}}
When no more audio will be sent, send an empty binary frame (0 bytes) and KEEP THE WEBSOCKET OPEN. The server finalizes any pending utterances (including ones still in interim state), streams the trailing is_final results back, then closes the WebSocket. Closing the WebSocket without this marker drops the trailing finals — WebSocket has no half-close, so once the client closes the server cannot deliver the remaining results. Finalize completes within ~5 seconds after the EOS marker. After the trailing finals the server sends a terminal frame ({"finished": true} for JSON, disconnect_reason=SESSION_ENDED for protobuf) and then closes.
ws.send(b'') # Python
ws.send(new ArrayBuffer(0)); // JavaScript
Examples
Python
import asyncio
import json
import websockets
async def live_stt(api_key: str):
url = f"wss://extapi.bizcrush.ai/v1/stt/stream?api_key={api_key}&format=json"
async with websockets.connect(url) as ws:
await ws.send(json.dumps({"encoding": "pcm16"}))
resp = json.loads(await ws.recv())
assert resp["connected"], f"Connection failed: {resp}"
async def send_audio():
with open("audio.pcm", "rb") as f:
while chunk := f.read(640):
await ws.send(chunk)
await asyncio.sleep(0.02)
# End-of-audio marker. DO NOT close the WebSocket here — the
# server needs the connection open to deliver trailing finals.
# The server will close after finalize completes (~5 seconds).
await ws.send(b'')
async def receive_results():
try:
async for msg in ws:
data = json.loads(msg)
if "chunk" in data:
chunk = data["chunk"]
status = "FINAL" if chunk["is_final"] else "interim"
speaker = chunk.get("speaker", "?")
print(f"[{status}] (S{speaker}) {chunk['text']}")
except websockets.exceptions.ConnectionClosed:
pass
await asyncio.gather(send_audio(), receive_results())
asyncio.run(live_stt("YOUR_API_KEY"))
JavaScript (Browser)
const ws = new WebSocket(
"wss://extapi.bizcrush.ai/v1/stt/stream?api_key=YOUR_API_KEY&format=json"
);
ws.onopen = () => {
ws.send(JSON.stringify({ encoding: "pcm16" }));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.connected) {
console.log("Connected! Start sending audio...");
}
if (data.chunk) {
const { id, text, is_final, speaker } = data.chunk;
console.log(`[${is_final ? "FINAL" : "interim"}] (S${speaker ?? "?"}) ${text}`);
}
};
// Server closes the WebSocket after delivering trailing finals.
ws.onclose = () => {
console.log("Stream finalized by server");
};
function sendAudioChunk(pcmData) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(pcmData);
}
}
// Call this when you have no more audio to send.
// DO NOT call ws.close() — the server needs the socket open to deliver
// trailing finals, then closes it itself (~5 seconds).
function endStream() {
if (ws.readyState === WebSocket.OPEN) {
ws.send(new ArrayBuffer(0)); // end-of-audio marker
}
}
Browser microphone: WebM/Opus
Keep every dataavailable chunk in order, including the last chunk after stop(). Do not restart the recorder or resend its header on the same WebSocket.
const mimeType = "audio/webm;codecs=opus";
if (!MediaRecorder.isTypeSupported(mimeType)) throw new Error("Use PCM16 on this browser");
const stream = await navigator.mediaDevices.getUserMedia({audio: true});
const recorder = new MediaRecorder(stream, {mimeType});
const ws = new WebSocket("wss://extapi.bizcrush.ai/v1/stt/stream?format=json&api_key=YOUR_API_KEY");
let pending = Promise.resolve();
recorder.ondataavailable = ({data}) => {
if (data.size) pending = pending.then(async () => {
const bytes = await data.arrayBuffer();
if (ws.readyState === WebSocket.OPEN) ws.send(bytes);
});
};
recorder.onstop = async () => {
await pending;
if (ws.readyState === WebSocket.OPEN) ws.send(new ArrayBuffer(0));
stream.getTracks().forEach(track => track.stop());
};
ws.onopen = () => ws.send(JSON.stringify({"encoding": "opus", "container": "webm", "language_hints": ["en"]}));
ws.onmessage = ({data}) => {
const event = JSON.parse(data);
if (event.connected) recorder.start(100);
console.log(event);
};
ws.onclose = () => {
if (recorder.state !== "inactive") recorder.stop();
stream.getTracks().forEach(track => track.stop());
};
// Call recorder.stop() when the user finishes.File STT
Send an audio file URL and receive the full transcript with speaker diarization.
Complete WebM/Opus recordings are supported. Assemble all MediaRecorder chunks into one file and provide a downloadable URL; later chunks alone do not contain the required header.
/stt
Auth: ?api_key
Transcribe an audio file URL and return the full transcript with speaker diarization.
Request Body (JSON)
| Name | Type | Required | Description |
|---|---|---|---|
| audio_url | string | required | Public audio file URL (signed URL, CDN URL, etc.) |
| session_id | string | optional | Session identifier. Auto-generated if omitted |
| enable_diarization | boolean | optional | Enable speaker diarization (default: true) |
| language_hints | string[] | optional | Language codes, e.g. ["ko","en"] or "ko,en". Up to 3; extras dropped. Omit for auto-detection. Legacy `language_hint` (single string) also accepted. |
| context_keywords | string[] | optional | Domain keywords/terms to bias recognition toward names, jargon, etc., e.g. ["Albert Gee","BizCrush"] or "Albert Gee,BizCrush". Up to 2000 characters total; extras dropped. |
Response Fields
| Field | Type | Description |
|---|---|---|
| text | string | Full transcript text |
| detected_language | string | Detected language code |
| confidence | float | Average confidence (0~1) |
| utterances | array | Speaker-diarized utterance segments |
| utterances[].speaker | string | Speaker number ("0", "1", "2", ...) |
| utterances[].text | string | Utterance text |
| utterances[].start_ms | integer | Start time (milliseconds) |
| utterances[].end_ms | integer | End time (milliseconds) |
| utterances[].confidence | float | Utterance confidence |
| utterances[].language | string | Utterance language |
Response Example
{
"text": "Could you introduce yourself and tell us about your role?",
"detected_language": "en",
"confidence": 0.961,
"utterances": [
{
"speaker": "1",
"text": "Could you introduce yourself and tell us about your role?",
"start_ms": 960,
"end_ms": 5400,
"confidence": 0.94,
"language": "en"
},
{
"speaker": "2",
"text": "Sure. My name is Ethan Kim, and I'm CTO of BizCrush.",
"start_ms": 5820,
"end_ms": 9600,
"confidence": 0.96,
"language": "en"
}
]
}
cURL Example
curl -X POST \
"https://extapi.bizcrush.ai/v1/stt?api_key=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"audio_url": "https://example.com/audio.oga", "enable_diarization": true}'
Note: Response time depends on audio length — may take up to several minutes. Set timeout to 600+ seconds.
Text-to-Speech (TTS)
Convert text to natural-sounding speech and receive a signed download URL (audio uploaded to Firebase Storage).
/tts
Auth: ?api_key
Generate speech audio from text and return a signed download URL (audio uploaded to Firebase Storage at tts/{session_id}/{message_id}.ogg).
Request Body (Protobuf)
application/x-protobuf (request and response are protobuf-encoded SessionTTSRequest / SessionTTSResponse)
| Name | Type | Required | Description |
|---|---|---|---|
| session_id | string | required | Session identifier |
| message_id | string | required | Message identifier (used in storage path) |
| text | string | required | Text to convert to speech |
Response Fields
| Field | Type | Description |
|---|---|---|
| audio_url | string | Signed download URL (Opus / OGG) — valid for several days |
| error | string | Set on failure; audio_url empty |
TTS Supported Languages
Text-to-speech generates natural-sounding audio across 57 languages. The language is detected automatically from the input text — no configuration required.
| Code | Language |
|---|---|
| en | English |
| zh | 中文 |
| hi | हिन्दी |
| es | Español |
| ar | العربية |
| fr | Français |
| pt | Português |
| ru | Русский |
| id | Bahasa Indonesia |
| de | Deutsch |
| ja | 日本語 |
| vi | Tiếng Việt |
| it | Italiano |
| ko | 한국어 |
| th | ภาษาไทย |
| af | Afrikaans |
| hy | Հայերեն |
| az | Azərbaycan |
| be | Беларуская |
| bs | Bosanski |
| bg | Български |
| ca | Català |
| hr | Hrvatski |
| cs | Čeština |
| da | Dansk |
| nl | Nederlands |
| et | Eesti |
| fi | Suomi |
| gl | Galego |
| el | Ελληνικά |
| he | עברית |
| hu | Magyar |
| is | Íslenska |
| kn | ಕನ್ನಡ |
| kk | Қазақ |
| lv | Latviešu |
| lt | Lietuvių |
| mk | Македонски |
| ms | Bahasa Melayu |
| mi | Te Reo Māori |
| mr | मराठी |
| ne | नेपाली |
| no | Norsk |
| fa | فارسی |
| pl | Polski |
| ro | Română |
| sr | Српски |
| sk | Slovenčina |
| sl | Slovenščina |
| sw | Kiswahili |
| sv | Svenska |
| tl | Tagalog |
| ta | தமிழ் |
| tr | Türkçe |
| uk | Українська |
| ur | اردو |
| cy | Cymraeg |
Supported Languages
Automatic language detection is built in — no configuration needed. The STT engine detects and transcribes across 60 languages seamlessly, even when speakers switch languages mid-conversation. Optionally use language_hint for faster detection.
| Code | Language |
|---|---|
| en | English |
| zh | 中文 |
| hi | हिन्दी |
| es | Español |
| ar | العربية |
| fr | Français |
| pt | Português |
| ru | Русский |
| id | Bahasa Indonesia |
| de | Deutsch |
| ja | 日本語 |
| vi | Tiếng Việt |
| it | Italiano |
| ko | 한국어 |
| th | ภาษาไทย |
| af | Afrikaans |
| sq | Shqip |
| az | Azərbaycan |
| eu | Euskara |
| be | Беларуская |
| bn | বাংলা |
| bs | Bosanski |
| bg | Български |
| ca | Català |
| hr | Hrvatski |
| cs | Čeština |
| da | Dansk |
| nl | Nederlands |
| et | Eesti |
| fi | Suomi |
| gl | Galego |
| el | Ελληνικά |
| gu | ગુજરાતી |
| he | עברית |
| hu | Magyar |
| kn | ಕನ್ನಡ |
| kk | Қазақ |
| lv | Latviešu |
| lt | Lietuvių |
| mk | Македонски |
| ms | Bahasa Melayu |
| ml | മലയാളം |
| mr | मराठी |
| no | Norsk |
| fa | فارسی |
| pl | Polski |
| pa | ਪੰਜਾਬੀ |
| ro | Română |
| sr | Српски |
| sk | Slovenčina |
| sl | Slovenščina |
| sw | Kiswahili |
| sv | Svenska |
| tl | Tagalog |
| ta | தமிழ் |
| te | తెలుగు |
| tr | Türkçe |
| uk | Українська |
| ur | اردو |
| cy | Cymraeg |
STT Error Codes
| Status | Error | Description |
|---|---|---|
| 401 | Missing authorization | API key not provided |
| 401 | Invalid API key | Invalid API key |
| 400 | Empty request body | Request body is empty |
| 400 | Missing audio_url | audio_url field missing (File STT) |
| 400 | Missing session_id, message_id, or text | Required TTS field missing |
| 400 | Invalid JSON | JSON parsing failed |
| 500 | TTS generation failed | OpenAI TTS call failed |
| 500 | Storage upload failed | Firebase Storage upload failed |
| 500 | (varies) | Internal server error (transcription failure, etc.) |
| WS 1008 | Auth failure | WebSocket closed with code 1008 on authentication failure |
Text Translation
Translate plain text into multiple target languages with BizCrush AI.
/v1/translate-text
Translate plain-text input while preserving document formatting.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| text | str | required | Plain text to translate (up to 100,000 characters by default) |
| target_languages | list[str] | required | Target language codes or names, such as ["ko", "en", "Japanese"] (up to 10) |
| source_language | str | optional | Source language code or name (default: auto) |