STT for Humans

Real-time speech-to-text for human conversations with krisp-realtime-stt-humans-v1: diarization, PII redaction and custom vocabulary.

🚧

Beta: Speech-to-Text is in beta. Models, messages and limits may change before general availability.

This page describes the WebSocket protocol for Krisp streaming speech-to-text (STT) with the krisp-realtime-stt-humans-v1 model, built for transcribing human conversations. It supports speaker diarization and PII redaction.

Endpoint

  • WebSocket URL: wss://streaming.krisp.ai/stt
  • Auth: temporary session token. See Authentication.

Use the session token as the API key when opening the WebSocket. Do not send the master token on the WebSocket.

Authorization: api-key <session-token>

If your client cannot set headers, pass the same value as the authorization query parameter. See the API overview.


Connection and handshake flow

1) Client initiates connection + sends handshake message

After opening the WebSocket connection with the session token, the client must send an initial JSON handshake that defines the session.

Handshake message (client → server, JSON):

{
  "model": "krisp-realtime-stt-humans-v1",
  "vocabulary": ["word1", "word2"],
  "sample_rate": 16000,
  "speaker_diarization": false,
  "pii_redaction": false,
  "channels": 1,
  "audio_format": "pcm_s16le",
  "language_hints": ["en"],
  "metadata": {}
}
FieldTypeDescription
modelstringkrisp-realtime-stt-humans-v1.
vocabularystring[]Optional list of custom vocabulary terms.
sample_ratenumberAudio sample rate in Hz. Must be 16000.
speaker_diarizationbooleanEnable speaker diarization. Default: false. See Diarization.
pii_redactionbooleanEnable PII redaction. Default: false.
channelsnumberNumber of audio channels. Use 1.
audio_formatstringpcm_s16le (PCM signed 16-bit little-endian, default) or opus.
metadataobjectOptional object echoed in the ready response. Maximum 4096 bytes when JSON-encoded.
language_hintsstring[]Optional. Limits which writing systems the transcript may use. It does not choose a language. Supported codes are en, de, es, fr, nl, pt and it (Latin script), and ru (adds Cyrillic), hy (adds Armenian) and zh (adds Chinese). A region suffix (en-US, zh-CN) and letter case are ignored. Latin is always allowed, and several hints allow the union of their scripts: for example, ["hy", "en"] allows Latin and Armenian. Any other code, or an empty list, applies no restriction. Text in any language that uses an allowed script can still appear. Invalid values never close the connection. See Supported languages.

2) Server acknowledges readiness (or returns an error)

Once the server validates the handshake and is ready to receive audio, it responds with a ready message.

Ready response (server → client, JSON):

{
  "type": "ready",
  "request_id": "9f2c...",
  "metadata": {}
}
  • request_id: unique identifier for the streaming session. Include it if you contact Krisp support.
  • metadata: echo of the metadata object from the handshake.

Do not send audio before this message. If no handshake is received within 15 seconds, the server closes the session.

Error response

If the server cannot accept the session, it responds with an error message and closes the connection. Besides the errors below, the common errors apply.

Error response (server → client, JSON):

{
  "type": "error",
  "request_id": "9f2c...",
  "error": {
    "code": "invalid_api_key",
    "status_code": 401,
    "message": "Invalid Authorization header.",
    "details": {
      "hint": "Check authorization header."
    }
  }
}
error.codestatus_codeDescription
missing_authorization_header401Authorization header is missing
invalid_api_key401Authorization header is malformed
unauthorized403Session token is not valid or has expired
invalid_client_hello400Handshake is missing or invalid
client_hello_timeout408Handshake was not received in time
chunk_too_large413A message exceeded 65,536 bytes
internal_server_error500Unexpected server error

Audio streaming

3) Client streams audio frames (raw bytes)

After receiving type: "ready", the client sends raw audio bytes as binary WebSocket messages.

  • Message type: binary (raw bytes)
  • Encoding: according to audio_format in the handshake: pcm_s16le (PCM signed 16-bit little-endian) or opus
  • Sample rate: 16,000 Hz
  • Channels: 1 (mono)
  • Recommended chunk duration: 80 ms, or multiples of 80 ms
  • Maximum message size: 65,536 bytes

Stream audio in realtime. Do not wrap audio in JSON.


Transcription messages

4) Server sends transcription results (JSON)

As the server processes audio, it returns transcription messages as JSON. Messages may be partial or final.

Example transcript response (server → client, JSON):

{
  "type": "transcript",
  "metadata": {},
  "transcript": {
    "words": [
      {
        "text": " weather",
        "start": 12480,
        "end": 12880,
        "language": "en",
        "language_confidence": 0.9921
      }
    ],
    "isFinal": true
  }
}
  • text keeps its leading space, so joining text in arrival order gives the transcript.
  • start and end bound the word, in milliseconds from the start of the stream. language and language_confidence are present when the server runs language ID (labels: see Supported languages).
  • metadata is always empty.
  • A final never changes and is never repeated.

5) Diarization

Add "speaker_diarization": true to the handshake:

{
  "model": "krisp-realtime-stt-humans-v1",
  "speaker_diarization": true
}

Diarization frames arrive between transcript frames:

{
  "type": "diarization",
  "committed": [{ "start": 463400, "end": 469400, "speaker": 5 }],
  "provisional": [
    { "start": 471500, "end": 498500, "speaker": 5 },
    { "start": 499100, "end": 502300, "speaker": 2 }
  ],
  "n_speakers": 6
}
FieldMeaning
committedSegments that became final since the previous frame. Append them; they never change and are never sent again.
provisionalThe current guess for everything after the last committed segment. Each frame's array replaces the previous one.
start, endInteger ms of stream time, on the same timeline as a word's start / end.
speakerInteger ID, stable for the whole session; not comparable across sessions.
n_speakersSpeakers in committed plus current provisional segments; can go down if a provisional speaker disappears.
lasttrue on the session's final diarization frame only.

A frame is sent only when something changed.

The current speaker timeline is committed followed by the latest provisional. The two never overlap.

A word belongs to the segment where start <= word.start < end:

  • Segment edges are snapped to word starts, so every word falls in exactly one segment.
  • Punctuation and word pieces stay with their word.
  • A word under a committed segment keeps its speaker for good. A word under a provisional segment can still change speaker.

Timing

WhatTypical
First (provisional) speaker label for a word2–4 s after speech
Committed labelSegment end + 26–33 s
Final frame after end_of_audio0.3–0.5 s

Show provisional labels straight away and repaint them as frames arrive. Don't wait for committed labels in a live UI: commits exist to give storage and analytics a guarantee that labels won't change.

Session end

After end_of_audio, frames arrive in this order:

  1. the remaining transcript frames;
  2. the final diarization frame with "last": true, after which every segment is committed;
  3. end.

The session is over at end, not at last. Closing the socket without end_of_audio loses the final diarization state.


6) Stream end

To end the stream, the client sends one text frame after its last audio message:

{"type": "end_of_audio"}

The server then finishes the session and closes the socket itself. The client shouldn't close it; it should read frames until end arrives.

After end_of_audio, the last frame the server sends is end, and it closes the socket right after:

{
  "type": "end",
  "language": {
    "overall_language": "en",
    "overall_confidence": 0.982,
    "current_language": "en",
    "current_confidence": 0.991,
    "per_language": {
      "de": 0.003,
      "en": 0.982,
      "es": 0.004,
      "fr": 0.002,
      "it": 0.001,
      "nl": 0.001,
      "pt": 0.001,
      "zh-CN": 0.001,
      "ru": 0.003,
      "hy-AM": 0.002
    }
  }
}

The last frame of a session. language is present when the server runs language ID:

  • overall_* averages the whole session.
  • current_* covers the last 0.5 s.
  • per_language holds the mean probability of every language ID label over the session (all ten, see Supported languages).

Supported languages

The model transcribes the ten languages below. Output is limited to Latin, Cyrillic, Armenian and Chinese characters, so languages written in other scripts are not transcribed. The language ID labels are the values of words[].language and the keys of end.language.per_language; language_hints takes the bare code.

Languagelanguage_hints codeScripts allowed by the hintLanguage ID label
EnglishenLatinen
GermandeLatinde
SpanishesLatines
FrenchfrLatinfr
ItalianitLatinit
DutchnlLatinnl
PortugueseptLatinpt
RussianruLatin, Cyrillicru
ArmenianhyLatin, Armenianhy-AM
Chinese (Mandarin)zhLatin, Chinesezh-CN

Several hints allow the union of their scripts. Hints covering Cyrillic, Armenian and Chinese together, an unknown code or an empty list allow every script. Language ID only knows these ten labels: speech in another language is labelled as the closest of them.


Example (Python)

Streams a 16 kHz, mono, 16-bit PCM WAV file at realtime pace with diarization on, and prints every message from the server. Requires pip install "websockets>=14".

import asyncio
import json
import sys
import wave

import websockets

SESSION_TOKEN = "YOUR_SESSION_TOKEN"  # see the Authentication page
URL = "wss://streaming.krisp.ai/stt"
CHUNK_MS = 480  # a multiple of 80 ms


async def main(path: str) -> None:
    headers = {"Authorization": f"api-key {SESSION_TOKEN}"}
    async with websockets.connect(URL, additional_headers=headers) as ws:
        await ws.send(json.dumps({
            "model": "krisp-realtime-stt-humans-v1",
            "sample_rate": 16000,
            "channels": 1,
            "audio_format": "pcm_s16le",
            "speaker_diarization": True,
        }))
        hello = json.loads(await ws.recv())
        if hello["type"] != "ready":
            raise RuntimeError(hello)

        async def send_audio() -> None:
            with wave.open(path, "rb") as wav:
                frames = 16000 * CHUNK_MS // 1000
                while chunk := wav.readframes(frames):
                    await ws.send(chunk)
                    await asyncio.sleep(CHUNK_MS / 1000)
            await ws.send(json.dumps({"type": "end_of_audio"}))

        sender = asyncio.create_task(send_audio())
        async for message in ws:
            msg = json.loads(message)
            print(msg)
            if msg.get("type") == "end":
                break
        await sender


asyncio.run(main(sys.argv[1]))

Changelog

  • 2026-10-01: Page created.

Did this page help you?