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.
Languages
Pass the language inlanguage_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
From your server
Send your API key in theAuthorization header, as on every other request:
?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 withPOST /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
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
- Connect with your settings on the URL. The first message you receive is
session.begin, with the settings in force and your limits. - Send audio in
audio_inputmessages as it arrives, about 20 to 100 ms at a time. - With manual endpointing, send
speech_startwhen the caller starts talking andspeech_endwhen they stop. With VAD endpointing, we find those moments and tell you withvad.speech_startandvad.speech_end. - Partial transcripts arrive while the utterance is open. After it ends, you
get exactly one
transcript.finalfor it. - Keep the socket for the rest of the call. Send
endwhen the call is over: we finish any open utterance, send its final, thensession.endwith 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, usemanual. If you
don’t, use vad.
Manual
Your own voice activity detection (Pipecat’s VAD, a turn model, your telephony provider’s events) decides. Sendspeech_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 sendvad.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 anevent field.
audio_input (shortened)
Control messages, one per frame
config.update
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
utterance_idx counts from 0 for each session. An utterance with no
recognisable speech still gets its final, with text set to "".
config.updated
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.beginis the first message, andsession.endthe last one when a session ends normally.- Every utterance gets exactly one
transcript.final, and finals arrive inutterance_idxorder, 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
Anerror 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
Sendend 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, withvia 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 yourspeech_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 afterspeech_end.
stream_stt.py
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.
Related
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.