Skip to main content
The streaming endpoint turns live audio into text while the person is still talking. You open one WebSocket per call and send audio as it arrives. Partial transcripts come back as the words are recognised, and each utterance ends with one final transcript. It runs Mira STT, our own speech recognition, the same one our voice agents listen with.

When to use it

Use the socket for audio that is still happening: a phone call, a browser microphone, anything where your code needs the words before the speaker has finished. Compared with sending clips over HTTP:
  • You set up the connection once per call.
  • Partial transcripts arrive while the person speaks, so you can show them or detect barge-in.
  • With manual endpointing, the final comes back straight after your speech_end, which is the fastest path for a voice agent.
For one clip that already exists, the HTTP endpoint is simpler. For a recording with speakers and timestamps, use offline transcription.
On Pipecat, MiraiSTTService from our Pipecat package speaks this protocol for you, including endpointing and reconnects.

Languages

Pass the language in language_code. The default is hi-IN. hi-IN (Hindi, including Hinglish), en-IN, gu-IN, mr-IN, pa-IN, bn-IN, ta-IN, te-IN, kn-IN and ml-IN are accepted, and so are the same codes without -IN. Mira STT writes speech the way it was spoken, with English words in Latin script, never translated. There is no automatic language detection: name the language, and change it mid-call with config.update if the caller switches. Languages has the status of each one.

Connect

Open one socket per call and keep it until the call ends.

From your server

Send your API key in the Authorization header, as on every other request:
We refuse an API key in the URL (?api_key= returns 401), because URLs end up in proxy and server logs.

From a browser

A browser cannot set headers on a WebSocket, and your API key must never reach a web page. Your backend mints a short-lived token with POST /v2/stt/stream/tokens and hands the page only the token. See Browser stream tokens.

Query parameters

Everything about the session is set on the URL, checked before the upgrade:
A phone call with your own VAD
An unknown parameter, a value outside these, or a parameter given twice fails the upgrade with 400 invalid_config, and message names it. token is the only other parameter we read. mode and stream_type are also accepted, for clients moving from Sarvam.

When the connection is refused

We check everything we can before the WebSocket upgrade. A refused connection gets an ordinary HTTP response with the usual error envelope: A successful upgrade carries X-Session-Id: sttws_…, the same id as session_id in session.begin.

How a session flows

  1. Connect with your settings on the URL. The first message you receive is session.begin, with the settings in force and your limits.
  2. Send audio in audio_input messages as it arrives, about 20 to 100 ms at a time.
  3. With manual endpointing, send speech_start when the caller starts talking and speech_end when they stop. With VAD endpointing, we find those moments and tell you with vad.speech_start and vad.speech_end.
  4. Partial transcripts arrive while the utterance is open. After it ends, you get exactly one transcript.final for it.
  5. Keep the socket for the rest of the call. Send end when the call is over: we finish any open utterance, send its final, then session.end with what the session cost, and close the socket.

Manual or VAD endpointing

Endpointing decides when an utterance ends, which is when its final transcript is produced. If you already have a VAD, use manual. If you don’t, use vad.

Manual

Your own voice activity detection (Pipecat’s VAD, a turn model, your telephony provider’s events) decides. Send speech_start when speech begins and speech_end when it ends, and the final comes back straight after speech_end. There is no silence wait on our side, which makes this the fastest path for a voice agent. Most VADs fire a little after the first syllable, so open the utterance with about half a second of the audio from before speech_start: send speech_start, then that buffered audio, then the live stream. Audio you send outside an utterance is billed but not transcribed. flush does the same as speech_end. One utterance can last at most 60 seconds. Past that, you get a fatal utterance_too_long error, the socket closes with 1011, and that utterance gets no final. Someone who talks for a long time without a pause your VAD catches can reach it, so send speech_end at a pause at least every 25 seconds, then speech_start for the next utterance.

VAD

We detect speech in your audio for you. You send audio continuously, and we send vad.speech_start when speech begins and vad.speech_end once the caller has been quiet for silence_duration_ms (1 second by default), then the final. Use vad.speech_start to detect barge-in. Long speech with no pause is cut into utterances for you, so the 60-second utterance limit never ends a vad session. The final can only start after that silence, so with the defaults it arrives at least a second after the caller stops. A lower silence_duration_ms gives quicker turns and cuts more people off mid-thought. You can switch between the two mid-session with config.update. The change applies from the next utterance.

Audio

Send telephone audio as it comes off the line: 8 kHz μ-law from Twilio or Plivo, 8 kHz PCM from Exotel. Don’t resample it to 16 kHz first; we do that on our side, and upsampling adds nothing the model can use. Send 16 kHz when your source is wideband, such as a browser microphone. Audio goes in JSON text frames, base64-encoded, in whole samples (an even number of bytes for linear16). Each audio_input carries at most 128,000 bytes of raw audio, counted before base64: 4 seconds of 16 kHz PCM, or 16 seconds of 8 kHz μ-law. A bigger one is a fatal audio_too_large, and the socket closes with 1011. Send audio at the pace it is captured. A file can be sent faster than real time, but a live call should stream as it happens so partials and finals stay in step with the speaker.

Messages you send

Every message is a JSON text frame with an event field.
audio_input (shortened)
Control messages, one per frame

config.update

Keys go at the top level, beside event. We check every key before applying any of them, so a bad value changes nothing and you get an invalid_config error. threshold, silence_duration_ms and min_speech_duration_ms apply at once. language_code, stream_type and endpointing apply from the next utterance, so an utterance in progress keeps the settings it started with. encoding, sample_rate and model are fixed for the life of the socket: open a new one to change them.

Events you receive

session.begin
config is the full configuration in force. turn_detection is your endpointing, and model is the build that is listening. Quote session_id when you ask us about a session; it is also on its row in your request log.
vad.speech_start
vad.speech_end
transcript.partial
transcript.final
A partial can rewrite words it sent earlier. Replace what you show with each new partial instead of appending to it, and replace the partials with the final. utterance_idx counts from 0 for each session. An utterance with no recognisable speech still gets its final, with text set to "".
config.updated
During an utterance, config still shows the settings that utterance runs with. A language change shows up from the next utterance.
pong
session.draining
reason is max_duration (the 1-hour limit), server_shutdown (our API server is restarting), insufficient_balance (your wallet ran out) or server_draining (the recogniser is restarting; that one also carries audio_samples_received and sample_rate). In every case the open utterance is finished and its final and session.end still arrive.
session.end
audio_seconds_billed and cost_paise are what this session was charged. audio_duration_s is all the audio you sent, silence included.
error

Ordering

These hold on every socket:
  • session.begin is the first message, and session.end the last one when a session ends normally.
  • Every utterance gets exactly one transcript.final, and finals arrive in utterance_idx order, even when a later utterance starts before an earlier final is back.
  • Every utterance gets its final, or a fatal error, within 5 seconds of its end. We never make up a transcript after an engine failure.
  • No partial for an utterance arrives after its final.

Errors on an open socket

An error with is_fatal: false refused one message and changed nothing; the session carries on. With is_fatal: true the session is over and the socket closes. No session.end follows, except after insufficient_balance, where the open utterance is finished first. The audio you sent is billed however the session ends, and the session appears in your request log with what it cost. Messages you send after end are ignored.

Closing

Send end when the call is over and wait for session.end: it carries the final for any utterance still open and the session’s cost. Closing the socket without end also ends the session; any utterance still waiting for its final is dropped. When we end a session ourselves (a restart, the 1-hour limit, or an empty wallet), you get session.draining first. Stop sending audio. We finish the open utterance, send its final, then session.end, and close. After a restart, open a new socket and carry on; pipecat-mirai does this for you. To stream for longer than an hour, open a new socket before the limit. The close frame’s reason text names the cause.

Limits

We send a WebSocket ping every 20 seconds and drop a client that has not answered for 60 seconds. Most WebSocket libraries answer pings for you. Pings do not count as activity for the idle limit: keep sending audio, or send {"event": "ping"} during long silences. If you need more than 10 sessions at once, ask your Mirai contact. It is a provisioning change and needs no change to your code.

Request log and billing

Every session has a row in your request log, with via set to ws, and a session longer than 5 minutes gets one row for each 5-minute part. You pay for the audio we received, silence included, however the session ends: session.end carries audio_seconds_billed and cost_paise, and Billing has the price and the ledger row. If your wallet runs out during a session, it ends with insufficient_balance after its last final (see Closing).

Latency

In a conversation, the number that matters is the gap between the end of speech and the final transcript, because your agent cannot answer before it has the words. With manual endpointing, the final follows your speech_end without a silence wait. With vad endpointing, add silence_duration_ms (1 second by default): the final cannot start until the caller has been quiet that long.

Python example

This script streams WAV files over one socket, one utterance per file, with manual endpointing. It paces the audio in real time, prints partials as they change and each final with how long it took after speech_end.
stream_stt.py
With websockets older than 14, pass extra_headers= instead of additional_headers=. A refused connection raises websockets.exceptions.InvalidStatus; its response.body holds the error as JSON. The latency it prints includes your network path to us. To make your own WAV from a phone recording, ffmpeg -i call.mp3 -ac 1 -ar 8000 -sample_fmt s16 call.wav.

STT quickstart

One clip over HTTP, billing, the request log and errors.

Languages

The ten codes, their status, and code-mixed speech.

Pipecat

MiraiSTTService, endpointing and the phone setup.

Streaming text to speech

The other half of a voice agent, on the same kind of socket.