Skip to navigation

Streaming

Stream audio over a WebSocket and receive transcripts in real time.

Murf Speech to Text uses a single WebSocket connection. You send audio as binary frames and control messages as JSON; the server sends back partial transcripts while the caller is speaking, and a final transcript when the turn ends.

Endpoint

model is required. Connect Finch-Indic (finch_indic_1.0) to wss://in.api.murf.ai/v1/transcription/stream-input, and Finch-Global (finch_global_1.0) to wss://us-east.api.murf.ai/v1/transcription/stream-input. The overview lists the languages each model transcribes.

wss://in.api.murf.ai/v1/transcription/stream-input
wss://us-east.api.murf.ai/v1/transcription/stream-input

Pass your API key as the api_key query parameter or api-key header. See Authentication.

Quickstart

1

Get an API key

Create a key in the Murf API dashboard and export it:

export MURF_API_KEY="your_api_key_here"
2

Install the packages

The examples use the websockets package (version 14 or later). The microphone example also needs pyaudio, which depends on PortAudio (brew install portaudio on macOS, sudo apt-get install portaudio19-dev on Debian/Ubuntu).

pip install websockets pyaudio
3

Stream audio and print transcripts

Streams a 16-bit PCM WAV file at real-time pace and prints each transcript. The WAV sample rate must be one of 8000, 12000, 16000, 24000, 32000, 44100 or 48000 Hz.

import asyncio
import json
import os
import wave
import websockets
API_KEY = os.environ["MURF_API_KEY"]
URL = f"wss://in.api.murf.ai/v1/transcription/stream-input?api_key={API_KEY}&model=finch_indic_1.0&language=hi"
CHUNK_MS = 100 # send 100 ms of audio per frame
async def send_audio(ws, path):
with wave.open(path, "rb") as wav:
# Declare the WAV's format. Sample rate must be one of:
# 8000, 12000, 16000, 24000, 32000, 44100, 48000.
await ws.send(json.dumps({
"type": "AudioConfig",
"codec": "pcm_s16le",
"sample_rate": wav.getframerate(),
"channels": wav.getnchannels(),
}))
frames_per_chunk = wav.getframerate() * CHUNK_MS // 1000
while chunk := wav.readframes(frames_per_chunk):
await ws.send(chunk)
await asyncio.sleep(CHUNK_MS / 1000) # pace the file like live audio
# Flush the remaining speech and close the connection with code 1000.
await ws.send(json.dumps({"type": "CloseStream"}))
async def receive_results(ws):
try:
async for raw in ws:
msg = json.loads(raw)
if msg["type"] in ("partial", "final"):
if msg["transcript"]:
print(f'[{msg["type"]}] {msg["transcript"]}')
if msg["type"] == "final":
print("-- end of turn --")
elif msg["type"] == "QuotaWarning":
print(f'Low balance: ${msg["remaining_funds"]}')
elif msg["type"] == "Error":
print(f'Error: {msg["message"]}')
except websockets.ConnectionClosedError:
pass # closed with an error code; printed below
print(f"Connection closed with code {ws.close_code}")
async def main(path):
async with websockets.connect(URL) as ws:
sender = asyncio.create_task(send_audio(ws, path))
await receive_results(ws)
sender.cancel()
if __name__ == "__main__":
asyncio.run(main("sample.wav"))

Session lifecycle

A connection is a session. A session can contain many turns, for example one per caller utterance in a voice agent.

StepYou sendThe server sends
ConnectWebSocket upgrade with your API keyNothing until audio arrives. An invalid key closes the connection with code 4001.
Declare format (optional)One AudioConfig message, before any audioNothing. An invalid config closes with 4002.
StreamBinary audio framespartial messages as the turn grows: StartOfTurn first, then Update. A final message when the caller pauses.
End a turn (optional){"type": "Finalize"}The turn’s transcript as a final message (EndOfTurn, trigger: "manual"). Send only if you want to end before the next pause. The connection stays open.
End the session{"type": "CloseStream"}A final message if a turn is still open, then close code 1000

Partial and final transcripts

Each message carries the transcript of the current turn in transcript. The same text is also in rawEvent.transcript.

  • partial: the transcript of the turn so far. Each partial contains the whole turn up to that point, so replace the text you are showing instead of appending to it.
  • final: the completed transcript of the turn. Use this text as the caller’s utterance, and read rawEvent.words if you need word timestamps.

Ending a turn

commit_mode sets how a turn ends. It accepts vad (default) or manual. Any other value closes the connection with code 4002 and the message commit_mode must be vad or manual.

  • vad (default)

    The server ends each turn when the caller pauses. While the caller speaks, it sends partial messages. When the caller stops speaking, the server waits briefly and then sends final with rawEvent.trigger set to vad. If the caller resumes speaking during that wait, the turn continues and final is held until the next pause.

    Sending {"type": "Finalize"} ends the current turn immediately, with trigger set to manual.

  • manual

    Use this mode when your agent or a turn-taking model decides when the caller has finished. The server still sends partial updates, but a pause does not end the turn. Send {"type": "Finalize"} to end it. If there was no pending speech, final is still sent with an empty transcript.

After a turn ends

Keep sending audio on the same connection. The next speech starts a new turn. rawEvent.turn_index goes up by one, rawEvent.sequence_id keeps counting, and audio_window_start / audio_window_end remain relative to the start of the session, so all turns share one timeline.

  • If your agent or a turn-taking model should decide when the caller has finished, connect with commit_mode=manual. The server still sends partial updates, but a pause does not end the turn. Send {"type": "Finalize"} to end it. If there was no pending speech, final is still sent with an empty transcript.
  • Any other commit_mode value closes the connection with code 4002 and the message commit_mode must be vad or manual.

Keeping the connection open

While you are not sending audio, for example while your agent is speaking, send a KeepAlive message so the connection is not closed for inactivity:

{ "type": "KeepAlive" }

Prefer KeepAlive over streaming silence. Silence still counts as audio on the wire.

If the server receives no audio and no KeepAlive for too long, it sends an Error message and closes the connection with code 4008. See Errors.

Reading results

A partial message:

{
"type": "partial",
"transcript": "मेरा नाम मर्फ़ है",
"rawEvent": {
"type": "TurnInfo",
"request_id": "a1b2c3d4-...",
"event": "Update",
"turn_index": 0,
"sequence_id": 4,
"audio_window_start": 4.20,
"audio_window_end": 5.25,
"transcript": "मेरा नाम मर्फ़ है"
}
}

A final message has the same shape. rawEvent.event is EndOfTurn, and rawEvent also includes trigger and words:

{
"type": "final",
"transcript": "मेरा नाम मर्फ़ है",
"rawEvent": {
"type": "TurnInfo",
"request_id": "a1b2c3d4-...",
"event": "EndOfTurn",
"turn_index": 0,
"sequence_id": 5,
"audio_window_start": 4.20,
"audio_window_end": 5.25,
"transcript": "मेरा नाम मर्फ़ है",
"trigger": "vad",
"words": [
{ "word": "मेरा", "start": 4.30, "end": 4.52 },
{ "word": "नाम", "start": 4.54, "end": 4.71 },
{ "word": "मर्फ़", "start": 4.75, "end": 5.12 },
{ "word": "है", "start": 5.15, "end": 5.25 }
]
}
}
FieldSent inDescription
typeEvery messagepartial while the turn is in progress, final when it ends.
transcriptEvery messageThe transcript of the current turn so far. Same as rawEvent.transcript.
rawEvent.eventEvery messageStartOfTurn, Update or EndOfTurn.
rawEvent.turn_indexEvery messageTurn number, starting at 0. Goes up by one after each EndOfTurn.
rawEvent.sequence_idEvery messageMessage counter that keeps increasing for the whole session.
rawEvent.audio_window_start / audio_window_endEvery messageStart and end of the turn, in seconds from the start of the session.
rawEvent.triggerfinal onlyvad when the server ended the turn after a pause. manual when the turn was ended by Finalize or CloseStream.
rawEvent.wordsfinal onlyStart and end of each word, in seconds from the start of the session.

Word times are already on the session timeline, so you don’t need to convert them. Quote rawEvent.request_id when you contact support.

Authentication

MethodExample
Querywss://in.api.murf.ai/v1/transcription/stream-input?api_key=YOUR_API_KEY&model=finch_indic_1.0&language=hi
Headerapi-key: YOUR_API_KEY on the WebSocket handshake

Speech to Text does not accept tokens from GET /v1/auth/token.

Query parameters

ParameterRequiredDefaultDescription
api_keyYes, unless you send the api-key header—Your Murf API key.
modelYes—finch_indic_1.0 on in.api.murf.ai, or finch_global_1.0 on us-east.api.murf.ai.
languageNoautoOne language value for the connected model, auto, or a comma-separated list of languages to detect from. See Languages.
keywordsNo—Comma-separated words or phrases to boost. See Keyword boosting.
alphaNo0.5How strongly to favour keywords, from 0.0 to 1.0.
text_formattingNofalseSet to true to format numbers and similar terms in Hindi and English (India) transcripts. See Text formatting.
bvcNofalseSet to true to remove background voices before transcription.
commit_modeNovadLeave unset. The server ends turns on a pause. Set manual only if you decide end of turn yourself. See Ending a turn.

Concurrency

PlanStreaming concurrency
Free tier5
Pay as you go20 by default; up to 150 on request
EnterpriseCustom (150+)

Each open Speech to Text connection uses one slot until it closes, whether or not you’re sending audio. Speech to Text limits are separate from Text to Speech concurrency. If you hit the limit, new connections close with code 4004. See Rate Limits & Concurrency for plan details.

Best practices

  • Replace partial transcripts, don’t append them. Each partial contains the whole turn so far. Use the final transcript as the completed turn.
  • Send audio in small frames. Send 20 to 100 ms of audio per frame. Larger frames delay transcription.
  • Stream at real-time pace when sending pre-recorded audio.
  • Send your telephony codec as-is. If your provider sends 8 kHz µ-law, declare mulaw at 8000 instead of converting the audio yourself. See Audio formats.
  • Wait for final after a pause. You do not need to send Finalize on each turn.
  • Keep one connection per call. Keep sending audio between turns rather than reconnecting.
  • Handle errors by close code, not by message text. See Errors.
  • Quote rawEvent.request_id when you contact support.

FAQs

Yes. partial messages show the transcript of the current turn while the caller is speaking. Each one replaces the previous one. When the turn ends, you receive a final message.

When the caller pauses, or as soon as you send Finalize. With commit_mode=manual, only when you send Finalize or CloseStream. See Ending a turn.

Yes, by streaming it over the WebSocket, as in the WAV example above. There is no separate file or batch transcription endpoint.

Stereo input is mixed down to mono before transcription. Each channel is not transcribed separately.

No. Speaker diarization is not available. Audio with several speakers is transcribed as a single stream.

No. Model, language, keywords and the audio format are set when you connect. Open a new connection to change them.

Audio is processed for transcription. See FAQ for how Murf handles data security and retention.

$0.12 per hour of audio, billed per second. Enterprise customers can get custom pricing. When your balance runs low, the server sends one QuotaWarning message per session. If credits run out, the connection closes with code 4005.

Next steps