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 you end the turn.

Endpoint

Speech to Text (Beta) is available in the India region only. Connect to wss://in.api.murf.ai/v1/transcription/stream-input.

wss://in.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}&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
End a turn{"type": "Finalize"}The turn’s transcript as a final message (EndOfTurn). 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 with Finalize

The server does not end a turn on silence. It keeps sending partial updates, and the turn ends only when you send Finalize or CloseStream. In a voice agent, send Finalize when your own end-of-turn detection fires: the server transcribes any pending speech immediately and sends a final message with rawEvent.event set to EndOfTurn and rawEvent.trigger set to manual.

A final message is sent only after you send Finalize or CloseStream. It is not sent automatically when the speaker pauses. If your code waits for final to decide that the caller has finished, make sure your code also sends Finalize.

If there was no pending speech when you sent Finalize, you still receive a final message. Its transcript is empty.

After Finalize, keep sending audio on the same connection to start the next 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.

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": "manual",
"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 onlymanual 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&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.
languageNoautoOne language value, 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.

Coming soon

The following parameters are accepted on the connection URL but have no effect yet. Do not rely on them in production.

ParameterWhat it will doWhat happens today
commit_mode=autoEnd a turn automatically when the caller stops speakingIgnored. A turn ends only when you send Finalize or CloseStream.
bvc=trueRemove background voices before transcriptionIgnored.

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.
  • Use Finalize at the end of each turn in voice agents. The server does not end a turn on silence.
  • Keep one connection per call. Use Finalize 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 you send Finalize or CloseStream. The server does not end a turn on silence. Automatic end of turn (commit_mode=auto) is coming soon.

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. 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.24 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