Realtime Speech-to-Text (ASR) API

Overview

Real-time speech-to-text streaming over a single WebSocket connection.

This endpoint is a dedicated transcription stream: you push raw audio frames in and receive incremental transcriptions and, optionally, translations back as JSON.

Palabra API client

Consider using the Palabra API Python client and checking the code example with it.

Key Setup Steps

Step 1: Authentication

Create an API Key to authenticate requests.

Step 2: WebSocket Connection

Connect to the endpoint with the required parameters:

wss://stream.palabra.ai/asr/v1/speech-to-text/stream?token=<API_KEY>&language=en&format=pcm_s16le&sample_rate=16000

Step 3: Audio Transmission

Send audio as raw binary WebSocket frames, with 320ms chunks recommended.

Step 4: Message Reception

The server returns JSON text frames with transcription data, identified by message_type.

Supported Parameters

Apply values as WebSocket query parameters

ParameterRequiredPurpose
api_keyYesAPI Key
formatYesAudio format specification
sample_rateConditionalHz rate for PCM formats
languageNoSource language code (defaults to auto)
translate_languagesNoComma-separated target languages
enable_filler_filterNoFilter control (true by default)
finalization_modeNoauto (default) or manual — see Finalization modes
finalization_timeoutNoManual mode safety-net timeout in seconds (default 60)

turn_detection and turn_timeout are deprecated aliases of finalization_mode and finalization_timeout.

Supported Languages

13 languages are supported. Automatic language detection operates in experimental mode.

CodeLanguage
arArabic
deGerman
enEnglish
esSpanish
frFrench
hiHindi
itItalian
jaJapanese
koKorean
nlDutch
ptPortuguese
ruRussian
zhChinese

Audio Formats

formatsample_rateNotes
pcm_s16leonly if ≠ 1600016-bit signed little-endian PCM. Recommended
pcm_f32le / pcm_f32berequired32-bit float PCM
pcm_s32le / pcm_s32berequired32-bit signed PCM
mulaw / alawrequiredG.711
webm / mp3 / aac / ogg / flac / wavnot usedContainer formats; rate is read from the stream

The finalize command

Besides binary audio, the client may send a JSON text frame with the finalize command — a manual end-of-segment:

{ "message_type": "finalize" }

It works in both finalization modes: the recognizer finalizes everything received so far and emits the pending segment as final (is_eos: true); recognition then continues as usual. Send it whenever your application knows the speaker's turn is over (push-to-talk release, your own turn detection), or after the last byte of a pre-recorded file (which often ends abruptly, with no trailing silence — so automatic, silence-driven segmentation would never finalize the tail).

The <fin> marker

Every finalize is answered. The final transcript that answers it carries the <fin> marker appended (without a space) to its text — that is how the client knows up to which point the transcription has been finalized:

{ "message_type": "transcription", "is_eos": true, "segment": { "text": "Hello world.<fin>", ... }, ... }

If the finalize produced no text (nothing new was recognized yet), the server still answers with an ordinary transcription message (is_eos: false) whose text is just the marker:

{ "message_type": "transcription", "is_eos": false, "segment": { "text": "<fin>", ... }, ... }

The marker never appears in translated_transcription messages.

Finalization modes

Segmentation ("when is the phrase finished?") is controlled by the finalization_mode query parameter:

  • auto (default) — segments are finalized automatically. This is the current behavior and requires nothing from the client.
  • manual — automatic finalization is suppressed: the client decides where turns end and sends the finalize command. As a safety net, if no finalize arrives within finalization_timeout seconds (default 60) since the last segment end, the nearest automatic end-of-segment signal is let through once; every finalize (and every delivered final segment) restarts the timer, so a client that keeps finalizing never hits it.

Use manual when your application has better knowledge of turn boundaries than the audio itself — push-to-talk UIs, external diarization/turn-taking logic, or benchmarks that need reproducible segmentation.

Message Types

Transcription Messages

Transcriptions deliver incremental text with segment timing and delta hints showing newly-added text.

{
  "message_type": "transcription",
  "transcription_id": "a1b2c3d4",
  "language": "en",
  "is_eos": false,
  "segment": {
    "text": "Hello world how are",
    "start_time": 0.32,
    "end_time": 1.84
  },
  "delta": {
    "text": "how are",
    "start_time": 1.20,
    "end_time": 1.84
  }
}
FieldDescription
transcription_idStable id for the segment. All messages of one segment share the same id. A new id means a new segment has started
languageDetected (or configured) source language of this segment
is_eosfalse — partial; the segment is still being updated. true — the segment is committed and final
segment.textThe full text of the segment so far
segment.start_time / end_timeSegment timing, in seconds relative to session start
deltaIncremental hint: the text added since the previous partial of the same segment (see below)

Working with delta

When the filler filter is disabled, delta.text is append-only: each transcription message carries exactly the text appended since the previous partial, so you can concatenate deltas directly.

With the enable_filler_filter enabled, the recognizer's tail might be rewritten mid-segment, which breaks the append relationship. In that mode treat segment.text as authoritative and overwrite the current segment on each message; use delta only as a hint.

Translated Transcription Messages

Translated Transcription appear only when translate_languages is set, once per target language, after each final (is_eos: true) transcription.

{
  "message_type": "translated_transcription",
  "transcription_id": "a1b2c3d4",
  "language": "es",
  "is_eos": true,
  "segment": {
    "text": "Hola mundo, ¿cómo estás?",
    "start_time": 0.32,
    "end_time": 1.84
  }
}

transcription_id matches the id of the source transcription (the is_eos: true one) this translation was produced from — use it to correlate a translation back to its original segment. language here is the target language, and is_eos is always true (translations are produced only for finalized segments).


Errors

Authentication and routing failures are reported as HTTP status codes during the WebSocket upgrade, before the connection is established:

HTTP statusMeaning
401Missing or invalid API Key / token
409A session is already active for this identity

After a successful upgrade, the server does not send application-level error messages over the wire — it closes the connection with a standard WebSocket close frame.


Complete examples

Microphone, automatic finalization

Streams microphone audio and prints transcriptions (and translations, if PALABRA_LANGUAGE targets are configured).

pip install pyaudio websockets
export PALABRA_API_KEY=...        # from Step 1
export PALABRA_LANGUAGE=en        # source language
import json
import os
import asyncio
import threading
import queue

import pyaudio
import websockets

WS_URL = "wss://stream.palabra.ai/asr/v1/speech-to-text/stream"
LANGUAGE = os.environ.get("PALABRA_LANGUAGE", "en")

SAMPLE_RATE = 16000
CHANNELS = 1
CHUNK = 5120  # samples ≈ 320 ms at 16 kHz (recommended chunk size)


def mic_reader(audio_queue: queue.Queue, stop_event: threading.Event):
    pa = pyaudio.PyAudio()
    stream = pa.open(
        format=pyaudio.paInt16,
        channels=CHANNELS,
        rate=SAMPLE_RATE,
        input=True,
        frames_per_buffer=CHUNK,
    )
    print("Microphone open, speak now...")
    try:
        while not stop_event.is_set():
            audio_queue.put(stream.read(CHUNK, exception_on_overflow=False))
    finally:
        stream.stop_stream()
        stream.close()
        pa.terminate()


async def stream(key: str):
    url = (
        f"{WS_URL}?token={key}&language={LANGUAGE}"
        f"&format=pcm_s16le&sample_rate={SAMPLE_RATE}"
    )

    audio_queue: queue.Queue = queue.Queue()
    stop_event = threading.Event()
    threading.Thread(
        target=mic_reader, args=(audio_queue, stop_event), daemon=True
    ).start()

    async with websockets.connect(url) as ws:
        print("Connected")

        async def send_audio():
            loop = asyncio.get_event_loop()
            while True:
                data = await loop.run_in_executor(None, audio_queue.get)
                await ws.send(data)  # raw binary frame

        async def receive():
            async for message in ws:
                msg = json.loads(message)
                msg_type = msg.get("message_type")

                if msg_type == "transcription":
                    text = msg["segment"]["text"]
                    tid = msg.get("transcription_id", "")
                    if msg.get("is_eos"):
                        print(f"\n[EOS] {text} [{tid}]")
                    else:
                        # segment.text is the source of truth — render it whole
                        print(f"\r      {text}", end="", flush=True)

                elif msg_type == "translated_transcription":
                    lang = msg.get("language", "?")
                    tid = msg.get("transcription_id", "")
                    print(f"\n[{lang}] {msg['segment']['text']} [{tid}]")

        try:
            await asyncio.gather(send_audio(), receive())
        finally:
            stop_event.set()


if __name__ == "__main__":
    try:
        asyncio.run(stream(os.environ["PALABRA_API_KEY"]))
    except KeyboardInterrupt:
        print("\nStopped.")

Pre-recorded file, manual finalize

Streams a 16-bit mono WAV file in finalization_mode=manual, sends the finalize command after the last chunk, and exits once the answering final transcription (marked with <fin>) arrives. A pre-recorded file usually ends abruptly, with no trailing silence, so without the finalize the tail of the recording would never be finalized.

pip install websockets
export PALABRA_API_KEY=...        # from Step 1
export PALABRA_LANGUAGE=en        # source language
python transcribe_file.py speech.wav
import asyncio
import json
import os
import sys
import wave

import websockets

WS_URL = "wss://stream.palabra.ai/asr/v1/speech-to-text/stream"
LANGUAGE = os.environ.get("PALABRA_LANGUAGE", "en")

CHUNK_MS = 320  # recommended chunk duration


async def transcribe_file(token: str, path: str):
    wav = wave.open(path, "rb")
    assert wav.getnchannels() == 1 and wav.getsampwidth() == 2, "need 16-bit mono WAV"
    sample_rate = wav.getframerate()
    chunk_frames = sample_rate * CHUNK_MS // 1000

    url = (
        f"{WS_URL}?token={token}&language={LANGUAGE}"
        f"&format=pcm_s16le&sample_rate={sample_rate}"
        "&finalization_mode=manual"
    )

    async with websockets.connect(url) as ws:
        print("Connected")

        async def send_audio():
            while True:
                data = wav.readframes(chunk_frames)
                if not data:
                    break
                await ws.send(data)  # raw binary frame
                await asyncio.sleep(CHUNK_MS / 1000)  # real-time pace
            # The file is over — no trailing silence will ever arrive,
            # so tell the recognizer the turn is done:
            await ws.send(json.dumps({"message_type": "finalize"}))
            print("\n(finalize sent)")

        async def receive():
            async for message in ws:
                msg = json.loads(message)
                if msg.get("message_type") != "transcription":
                    continue
                text = msg["segment"]["text"]
                if msg.get("is_eos"):
                    print(f"[final] {text}")
                else:
                    print(f"\r        {text}", end="", flush=True)
                if "<fin>" in text:
                    # the answer to our finalize — everything sent
                    # before it has now been transcribed
                    return

        await asyncio.gather(send_audio(), receive())


if __name__ == "__main__":
    asyncio.run(
        transcribe_file(os.environ["PALABRA_API_KEY"], sys.argv[1])
    )