Reference

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 receive partial / final transcripts 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

ProviderFiles (/stt/transcribe)Realtime (/stt/stream)Model overrideNotes
sonioxYesYesYesPlatform default. Supports domain context and language identification.
deepgramYesYesYesRich formatting flags; realtime endpointing tuning.
sarvamYesYes — 16 kHz onlyYesIndian-language focus; language_code override.
googleYesYesNo — default model onlyA model value is rejected with 400 (batch) / close 4400 (stream).
openaiYes (Whisper)No — rejectedYesNo 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

FieldTypeDefaultDescription
contextstringnoneDomain 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.
modelstringplatform defaultSoniox 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_identificationboolprovider defaultDetect 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:

soniox-context.json
json
{
  "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

FieldTypeDefaultDescription
modelstringnova-3-generalDeepgram model id, e.g. nova-3-medical.
smart_formatboolonFormat phone numbers, dates, currency.
punctuateboolonAdd punctuation.
numeralsboolonConvert spoken numbers to digits (important for lakhs/crores).
profanity_filterbooloffMask profanity.
diarizebooloffLabel different speakers.
auto_detect_languagebooloffLet Deepgram detect the language.
endpointing_msint | bool25Realtime only. Silence (ms) before a segment finalizes; true = Deepgram default, false = off.
utterance_end_msint ≥ 1000offRealtime 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

FieldTypeDefaultDescription
modelstringplatform defaultSarvam model id, e.g. saaras:v3.
language_codestringplatform defaultSarvam language code, e.g. hi-IN.

Transcribe a file

POST /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

FieldTypeRequiredDescription
audiofileyesThe audio clip.
providerstringyessoniox, deepgram, sarvam, openai, or google. Case-insensitive, trimmed.
modelstringnoFlat model shortcut. Rejected for google (400). Blank = provider default.
languagestringnoSingle language hint, e.g. en or hi. Blank = auto.
sonioxJSON stringnoSoniox config, e.g. {"context": "Breeze Buddy, UPI"}.
deepgramJSON stringnoDeepgram config. Realtime-only knobs are ignored here.
sarvamJSON stringnoSarvam config.

Example request

transcribe.sh
bash
# 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

json
{
  "text": "Hi, I'm calling about the UPI autopay setup on my Breeze Buddy account.",
  "provider": "soniox",
  "model": "stt-async-v2"
}
FieldTypeDescription
textstringThe transcript.
providerstringProvider that actually produced the text — openai when an unpinned request fell back to Whisper.
modelstring | nullModel that actually ran.

Errors

StatusMeaningCommon cause
400Bad RequestEmpty upload, or model sent with provider=google.
401 / 403UnauthorizedMissing or expired JWT — log in again.
413Payload Too LargeClip exceeds the 10 MB limit.
422Unprocessable EntityUnknown provider, malformed nested config JSON, or oversized config field (64 KB cap).
502Bad GatewayStrict mode: the pinned provider/model/options failed — nothing fell back.

Stream realtime audio

WS /stt/stream

Protocol

  1. Connect with the JWT: wss://<host>/agent/voice/breeze-buddy/stt/stream?token=<JWT> (or an Authorization: Bearer header from non-browser clients).
  2. Send one JSON text message — the stream config (below) — within 10 seconds.
  3. The server replies {"type": "ready"}. From here, send binary frames of raw PCM16 mono, little-endian audio at your configured sample_rate. Keep frames under 256 KB (a real mic pipeline sends a few KB every ~40 ms).
  4. The server pushes partial and final transcript events as the provider produces them.
  5. Send {"type": "stop"} (or just disconnect) to end. The server flushes any trailing final events, then closes with code 1000.

Stream config message

FieldTypeRequiredDescription
providerstringyessoniox, deepgram, sarvam, or google. openai is rejected (no realtime path).
modelstringnoFlat model shortcut. Rejected for google.
languagestring | string[]noHint code or list of codes (list support is provider-dependent, e.g. Soniox hints).
sample_rateintnoRate (Hz) of your PCM16 frames. 8000–48000, default 16000. Sarvam requires 16000.
soniox / deepgram / sarvamobjectnoThe provider configs above, passed through to the streaming service.
config-message.json
json
{
  "provider": "deepgram",
  "model": "nova-3-general",
  "language": "en",
  "sample_rate": 16000,
  "deepgram": { "smart_format": true, "endpointing_ms": 25 }
}

Server events

EventPayloadMeaning
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

CodeMeaning
1000Normal end — client stopped or disconnected.
4400Bad request: invalid config, unsupported provider, oversized audio frame, or the provider refused the connection (bad model, missing key).
4401Unauthorized: missing/expired token. Log in again and reconnect.
4408Timeout: no config within 10 s, no audio for 60 s, or the 300 s max stream length was reached.
1011Server-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).

token-endpoint.js (your backend)
javascript
// 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 });
});
stream-mic.js (browser)
javascript
// 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.

Was this helpful?