Transcription API
Standalone speech-to-text endpoints — one-shot file transcription and realtime microphone streaming over WebSocket.
Standalone speech-to-text, independent of any agent, template, or widget session. Two endpoints under /agent/voice/breeze-buddy:
POST /stt/transcribe— upload an audio clip, get the transcript back in one response.WS /stt/stream— stream raw microphone audio and receivepartial/finaltranscripts in realtime.
Both take the STT provider and its tuning directly in the request, using the same provider config models templates use (see STT in the voice pipeline). You can try every option on this page interactively on the console’s Transcription page.
Auth: always the temporary JWT from /login
Both endpoints authenticate with the short-lived JWT you get from POST /login — the same temporary token the console holds for your session. Send it as Authorization: Bearer <token>. Browsers cannot set headers on a WebSocket, so the stream also accepts the token as a ?token= query parameter. Tokens expire; call /login again to get a fresh one — there is no refresh endpoint.
Credentials never reach the browser
Your login email and password are secrets — never embed them in client-side code or call /login from the browser. In a browser integration, your backend acts as the proxy: it holds the credentials, calls /login server-side, and hands only the temporary token to your signed-in user’s browser. The token is safe to expose because it is short-lived and scoped; the credentials are not. See the browser integration example below.
Providers at a glance
| Provider | Files (/stt/transcribe) | Realtime (/stt/stream) | Model override | Notes |
|---|---|---|---|---|
soniox | Yes | Yes | Yes | Platform default. Supports domain context and language identification. |
deepgram | Yes | Yes | Yes | Rich formatting flags; realtime endpointing tuning. |
sarvam | Yes | Yes — 16 kHz only | Yes | Indian-language focus; language_code override. |
google | Yes | Yes | No — default model only | A model value is rejected with 400 (batch) / close 4400 (stream). |
openai | Yes (Whisper) | No — rejected | Yes | No realtime path; the stream closes with 4400. |
Model precedence and strict mode
Every request accepts a flat model shortcut and a nested per-provider config (soniox / deepgram / sarvam) that can itself carry a model. A model explicitly set in the selected provider’s nested config wins; otherwise the flat model fills in; otherwise the provider’s platform default runs. Nested configs for providers other than the selected one are ignored.
Pinned requests fail loudly — no silent fallback
A request with no model and no provider options is allowed to degrade: if the chosen provider is unavailable, the platform transcribes with OpenAI Whisper instead and the response reports what actually ran. The moment you pin a model or any nested provider option, the request runs strict: provider failure returns 502 (batch) or an error event (stream) instead of a Whisper transcript. You never get a result produced by something other than what you asked for.
Provider configuration
These nested objects are accepted by both endpoints. In the multipart batch request they are JSON-encoded strings; on the stream they are plain JSON objects inside the config message.
soniox
| Field | Type | Default | Description |
|---|---|---|---|
context | string | none | Domain adaptation. Realtime requires Soniox’s structured context as a JSON string (see below); anything unparseable is dropped. The batch API takes a plain string verbatim. |
model | string | platform default | Soniox model id — realtime: stt-rt-v5, stt-rt-v4 (platform default), stt-rt-preview; files: stt-async-v5 (platform default), stt-async-preview. |
enable_language_identification | bool | provider default | Detect the spoken language automatically; detected language rides on stream events. |
Realtime context is a JSON string whose parsed shape follows Soniox’s context object — text (free-form domain description), terms (vocabulary list), general (key–value pairs), translation_terms:
{
"provider": "soniox",
"soniox": {
"context": "{\"text\": \"Customer support calls for Breeze Buddy\", \"terms\": [\"Breeze Buddy\", \"UPI autopay\"], \"general\": [{\"key\": \"domain\", \"value\": \"payments\"}]}"
}
}Language hints on realtime streams are enforced strictly whenever language is set — Soniox transcribes only in the hinted languages. The console’s Transcription page has a context builder that produces this structure for you.
deepgram
| Field | Type | Default | Description |
|---|---|---|---|
model | string | nova-3-general | Deepgram model id, e.g. nova-3-medical. |
smart_format | bool | on | Format phone numbers, dates, currency. |
punctuate | bool | on | Add punctuation. |
numerals | bool | on | Convert spoken numbers to digits (important for lakhs/crores). |
profanity_filter | bool | off | Mask profanity. |
diarize | bool | off | Label different speakers. |
auto_detect_language | bool | off | Let Deepgram detect the language. |
endpointing_ms | int | bool | 25 | Realtime only. Silence (ms) before a segment finalizes; true = Deepgram default, false = off. |
utterance_end_ms | int ≥ 1000 | off | Realtime only. Max silence after the last word before an utterance ends. |
Only the fields you actually send are forwarded — omitted fields keep provider defaults and do not flip the request into strict mode.
sarvam
| Field | Type | Default | Description |
|---|---|---|---|
model | string | platform default | Sarvam model id, e.g. saaras:v3. |
language_code | string | platform default | Sarvam language code, e.g. hi-IN. |
Transcribe a file
/stt/transcribe multipart/form-data with the clip in the audio field. Common formats work — WAV, MP3, WebM/Opus, M4A, OGG, FLAC. The upload limit is 10 MB by default (platform-configurable).
Form fields
| Field | Type | Required | Description |
|---|---|---|---|
audio | file | yes | The audio clip. |
provider | string | yes | soniox, deepgram, sarvam, openai, or google. Case-insensitive, trimmed. |
model | string | no | Flat model shortcut. Rejected for google (400). Blank = provider default. |
language | string | no | Single language hint, e.g. en or hi. Blank = auto. |
soniox | JSON string | no | Soniox config, e.g. {"context": "Breeze Buddy, UPI"}. |
deepgram | JSON string | no | Deepgram config. Realtime-only knobs are ignored here. |
sarvam | JSON string | no | Sarvam config. |
Example request
# 1. Get a temporary JWT from the login API (server-side only — the
# email/password are credentials and must never reach client code)
TOKEN=$(curl -s -X POST $BASE_URL/agent/voice/breeze-buddy/login \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "password": "..."}' | jq -r .access_token)
# 2. Transcribe a clip (Soniox with domain context — a pinned, strict request)
curl -X POST $BASE_URL/agent/voice/breeze-buddy/stt/transcribe \
-H "Authorization: Bearer $TOKEN" \
-F "[email protected]" \
-F "provider=soniox" \
-F 'soniox={"context": "Breeze Buddy, Juspay, UPI autopay"}'Response — 200 OK
{
"text": "Hi, I'm calling about the UPI autopay setup on my Breeze Buddy account.",
"provider": "soniox",
"model": "stt-async-v2"
}| Field | Type | Description |
|---|---|---|
text | string | The transcript. |
provider | string | Provider that actually produced the text — openai when an unpinned request fell back to Whisper. |
model | string | null | Model that actually ran. |
Errors
| Status | Meaning | Common cause |
|---|---|---|
400 | Bad Request | Empty upload, or model sent with provider=google. |
401 / 403 | Unauthorized | Missing or expired JWT — log in again. |
413 | Payload Too Large | Clip exceeds the 10 MB limit. |
422 | Unprocessable Entity | Unknown provider, malformed nested config JSON, or oversized config field (64 KB cap). |
502 | Bad Gateway | Strict mode: the pinned provider/model/options failed — nothing fell back. |
Stream realtime audio
/stt/stream Protocol
- Connect with the JWT:
wss://<host>/agent/voice/breeze-buddy/stt/stream?token=<JWT>(or anAuthorization: Bearerheader from non-browser clients). - Send one JSON text message — the stream config (below) — within 10 seconds.
- The server replies
{"type": "ready"}. From here, send binary frames of raw PCM16 mono, little-endian audio at your configuredsample_rate. Keep frames under 256 KB (a real mic pipeline sends a few KB every ~40 ms). - The server pushes
partialandfinaltranscript events as the provider produces them. - Send
{"type": "stop"}(or just disconnect) to end. The server flushes any trailingfinalevents, then closes with code1000.
Stream config message
| Field | Type | Required | Description |
|---|---|---|---|
provider | string | yes | soniox, deepgram, sarvam, or google. openai is rejected (no realtime path). |
model | string | no | Flat model shortcut. Rejected for google. |
language | string | string[] | no | Hint code or list of codes (list support is provider-dependent, e.g. Soniox hints). |
sample_rate | int | no | Rate (Hz) of your PCM16 frames. 8000–48000, default 16000. Sarvam requires 16000. |
soniox / deepgram / sarvam | object | no | The provider configs above, passed through to the streaming service. |
{
"provider": "deepgram",
"model": "nova-3-general",
"language": "en",
"sample_rate": 16000,
"deepgram": { "smart_format": true, "endpointing_ms": 25 }
}Server events
| Event | Payload | Meaning |
|---|---|---|
ready | {"type": "ready"} | Config accepted, provider connected — start sending audio. |
partial | {"type": "partial", "text": "...", "language": "en"} | Interim hypothesis; will be revised. |
final | {"type": "final", "text": "...", "language": "en"} | Committed segment; append it to your transcript. |
error | {"type": "error", "message": "..."} | What went wrong — always sent before an error close. |
Close codes
| Code | Meaning |
|---|---|
1000 | Normal end — client stopped or disconnected. |
4400 | Bad request: invalid config, unsupported provider, oversized audio frame, or the provider refused the connection (bad model, missing key). |
4401 | Unauthorized: missing/expired token. Log in again and reconnect. |
4408 | Timeout: no config within 10 s, no audio for 60 s, or the 300 s max stream length was reached. |
1011 | Server-side failure mid-stream — e.g. the provider connection died. |
Idle (60 s), max duration (300 s), and the 10 MB batch cap are platform defaults and can be tuned via dynamic configuration.
Browser integration
Two pieces: your backend proxies /login (credentials stay server-side, only the temporary token goes out), and the browser uses that token for the stream. Cache the token server-side and re-login only when it expires. The console’s Transcription page implements the browser side end to end (AudioWorklet capture, resampling, buffering before ready).
// Your server is the credential proxy: email/password live in env vars
// here and NEVER ship to the browser. Gate this route with your own
// product's user auth, then hand out only the short-lived JWT.
app.get('/api/stt-token', requireYourAppAuth, async (req, res) => {
const login = await fetch(BREEZE_BASE + '/agent/voice/breeze-buddy/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: process.env.BREEZE_EMAIL,
password: process.env.BREEZE_PASSWORD
})
}).then((r) => r.json());
res.json({ access_token: login.access_token });
});// Browser: fetch the temporary token from YOUR backend — the browser
// never sees Breeze Buddy credentials and never calls /login itself.
const { access_token } = await fetch('/api/stt-token').then((r) => r.json());
// Connect the stream — browsers pass the token as a query param
const ws = new WebSocket(
WS_BASE + '/agent/voice/breeze-buddy/stt/stream?token=' + access_token
);
ws.binaryType = 'arraybuffer';
ws.onopen = () =>
ws.send(JSON.stringify({ provider: 'soniox', sample_rate: 16000, language: 'en' }));
ws.onmessage = (msg) => {
const event = JSON.parse(msg.data);
if (event.type === 'ready') startMic();
if (event.type === 'partial') renderInterim(event.text);
if (event.type === 'final') appendLine(event.text);
if (event.type === 'error') console.error(event.message);
};
// Mic -> PCM16 mono at 16 kHz -> binary frames
async function startMic() {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const ctx = new AudioContext({ sampleRate: 16000 });
const source = ctx.createMediaStreamSource(stream);
const tap = ctx.createScriptProcessor(4096, 1, 1); // use an AudioWorklet in production
tap.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;
}
if (ws.readyState === WebSocket.OPEN) ws.send(pcm.buffer);
};
source.connect(tap);
tap.connect(ctx.destination);
}
// End the session: server flushes trailing finals, then closes with 1000
function stop() {
ws.send(JSON.stringify({ type: 'stop' }));
}Try it without writing code
The console’s Transcription page exposes every field on this page — provider, model, language, sample rate, and all nested provider options — for both realtime streaming and file uploads. Use it to validate a configuration before wiring up your integration.