How to Stream Speech-to-Text over WebSocket
Streaming speech-to-text over a WebSocket takes four things: an access token, a short-lived WebSocket token, correctly framed 16 kHz mono PCM audio, and a loop that renders text as it arrives. This guide wires up all four, with code you can copy.
What you need before you start
- A Dictate account. The Free tier gives you 30 minutes of transcription with no credit card.
- An audio source that can emit raw PCM: a microphone capture, an existing streaming pipeline, or a WAV/MP3 file you chunk yourself.
- A WebSocket client. Anything that can send binary frames works -
websockets,ws,gorilla/websocket, or the browserWebSocketAPI.
Step 1: Create an account and keep the access token
Register, then log in. The login response contains an access_token. Store it and
send it as a bearer token on API calls:
POST /api/v1/auth/login
Content-Type: application/json
{"email": "you@company.com", "password": "your-password"}
The access token is short-lived by design. Refresh it with the refresh_token
from the same response rather than storing a long-lived secret.
Step 2: Request a WebSocket token
WebSocket connections cannot carry an Authorization header from a browser, so
Dictate issues a dedicated, deliberately short-lived token for the socket itself:
POST /api/v1/ws-token
Authorization: Bearer <access_token>
The response gives you everything needed to open the connection:
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"port": 443,
"path": "/ws",
"expires_in": 300,
"sample_rate": 16000,
"remaining_ms": 1800000
}
expires_inis 300 seconds. Request one per session rather than caching it; that keeps a leaked token from staying useful.sample_rateis 16000. Send audio at this rate.remaining_msis your remaining quota for this billing period, in milliseconds. Check it before starting a long session.
Use the ws_url field when the response provides one - it is assembled server-side
from the real request host, so you never have to guess the port or the TLS scheme.
Step 3: Open the WebSocket
Connect to /ws?token=YOUR_TOKEN. Two details matter: the connection must be
binary, and it must be opened before you start sending audio.
import asyncio, json, websockets
import requests
API = "https://dictate.top"
def get_ws_url():
token = requests.post(f"{API}/api/v1/auth/login", json={
"email": EMAIL, "password": PASSWORD
}).json()["access_token"]
info = requests.post(f"{API}/api/v1/ws-token", headers={
"Authorization": f"Bearer {token}"
}).json()
scheme = "wss" if info.get("ws_url", "").startswith("wss") else "ws"
host = info.get("host") or info.get("port")
return f"{scheme}://{host}{info['path']}?token={info['token']}"
async def main():
url = get_ws_url()
async with websockets.connect(url) as ws:
... # send audio, read results
asyncio.run(main())
In a browser, use binaryType = "arraybuffer" before you start receiving. Without
it, binary frames arrive as Blob objects and you will silently read nothing.
Step 4: Frame your audio correctly
This is where most integrations go wrong. Three rules:
- Format: 16 kHz, 16-bit signed, mono PCM. WAV, MP3 and OGG containers are accepted too.
- Frame size: 100-200 ms of audio per frame. At 16 kHz that is 3,200-6,400 bytes. Below 100 ms you pay framing overhead for no latency win; above 200 ms the results stop feeling live.
- Send as binary. JSON-encoding the audio and sending it as text roughly triples the payload and breaks the upstream protocol.
# 16 kHz * 2 bytes/sample = 32,000 bytes/second
# 150 ms frame = 32,000 * 0.15 = 4,800 bytes
FRAME_BYTES = 4800
ws.send(PCM_FRAME[:FRAME_BYTES]) # binary, not ws.send(json.dumps(...))
Send frames paced to real time. Dumping a ten-minute recording in one burst gets you the same transcript as speaking it, but none of the live behaviour that makes streaming worth using.
Step 5: Read the results as they arrive
Recognition text arrives incrementally on the same connection. Render each update as it comes in and replace the previous text, rather than appending - most streaming recognizers return the running transcript, not a delta.
async for message in ws:
if isinstance(message, str):
data = json.loads(message)
if "text" in data:
transcript_el.textContent = data["text"] # replace, not append
elif data.get("type") == "error":
print("session error:", data["message"])
Common failure modes
Connected, but no text comes back
Almost always one of three things, in this order: the token expired (they last 5 minutes), the audio is not 16 kHz 16-bit mono PCM, or the socket was opened as text instead of binary.
"Quota exceeded"
You have used the included duration for the billing period. Check remaining_ms from the token response, or open the dashboard.
Connection closes immediately
The token was already used or has expired. Tokens are single-use per issued session; fetch a new one for each connection attempt.
Text arrives, but it lags badly
Frames are too large or you are not pacing sends. Drop to 100-150 ms and send on a timer matched to the frame duration.
Where to go next
- The FAQ for billing, privacy and format details.
- What actually gets stored when you stream audio.
- Pricing if 30 minutes is not enough for your testing.