This is the full developer documentation for Reson8 Documentation # Getting Started > Get started with the Reson8 speech-to-text API: create an account, generate API keys, and transcribe prerecorded or realtime audio in ten languages. Welcome to the Reson8 API documentation. We provide speech-to-text infrastructure with real-time audio streaming and domain-specific model customization, in ten languages: Dutch, English, French, Frisian, German, Italian, Polish, Portuguese, Spanish, and Swedish. Follow these steps to get from zero to your first transcript: 1. **Create an account** Sign up at [console.reson8.dev](https://console.reson8.dev). From the console you can manage API clients and billing. New accounts start on a free plan with [usage limits](/limits/). 2. **Choose an endpoint** [Choosing an Endpoint](/choosing-an-endpoint/) helps you pick the right endpoint for your use case. You can try each one in the console playground before writing any code. 3. **Generate an API key** In the console, create a client and generate an API key for it. See [Authentication](/authentication/) for how to use the key in your requests. 4. **Make your first request** Call your endpoint of choice. For example, transcribing a file with the [prerecorded endpoint](/api/speech-to-text/prerecorded/): ```bash curl -X POST "https://api.reson8.dev/v1/speech-to-text/prerecorded" \ -H "Authorization: ApiKey " \ -H "Content-Type: application/octet-stream" \ --data-binary @recording.wav ``` The response contains the transcript: ```json { "text": "the patient presented with chest pain and shortness of breath" } ``` 5. **Go further** Add [speaker diarization](/speech-to-text/features/diarization/), bias transcription toward your domain with [custom models](/speech-to-text/features/custom-models/), recover structured tokens like order codes with [patterns](/speech-to-text/features/patterns/), or build a voice agent with the [LiveKit integration](/integrations/livekit/). Use the search bar at the top to find anything quickly. If you get stuck, [we’re happy to help](/support/). ## About Reson8 [Section titled “About Reson8”](#about-reson8) Reson8 is the speech-to-text provider built for complex terminology across European languages (including English). Where generic models mishear medication names, order numbers, or addresses, Reson8 adapts to your domain-specific terminology in real time or from a single text upload, no fine-tuning or audio samples needed. Teams use it for voice agents, medical scribes, meeting notetakers, contact center analysis, subtitling and captioning, legal transcription, and more. Reson8 is highly suitable for regulated industries, as it owns and operates its own GPU stack in the Netherlands, with zero subprocessors, zero audio retention, and no training on customer data. Fully ISO 27001 certified and GDPR compliant. # Request Token > POST /v1/auth/token - exchange a Reson8 API key for a short-lived access token. Request, response, and error codes. Exchange an API key for a short-lived access token. See [Authentication](/authentication/) for when to use tokens instead of API keys. ```plaintext POST https://api.reson8.dev/v1/auth/token ``` ## Request [Section titled “Request”](#request) ### Headers [Section titled “Headers”](#headers) | Header | Value | Description | | ------------- | ------------------ | ------------ | | Authorization | `ApiKey ` | Your API key | ### Example [Section titled “Example”](#example) * curl ```bash curl -X POST https://api.reson8.dev/v1/auth/token \ -H "Authorization: ApiKey " ``` * Python ```python import requests response = requests.post( "https://api.reson8.dev/v1/auth/token", headers={"Authorization": "ApiKey "}, ) token = response.json()["access_token"] ``` ## Response [Section titled “Response”](#response) `200 OK` ### Fields [Section titled “Fields”](#fields) | Field | Type | Description | | -------------- | ------ | ----------------------------------------------------------------------------------- | | `access_token` | string | The token to use in `Authorization` header | | `token_type` | string | Always `Bearer` | | `expires_in` | number | Token lifetime in seconds, currently 600 (10 minutes). Not configurable per request | ### Example [Section titled “Example”](#example-1) ```json { "access_token": "", "token_type": "Bearer", "expires_in": 600 } ``` ## Errors [Section titled “Errors”](#errors) | Status | Code | Description | | ------------------------- | ----------------------- | --------------------------------------------------------------- | | 401 Unauthorized | - | Missing or invalid API key. The response body is empty. | | 500 Internal Server Error | `internal_server_error` | Unexpected server error, returned as `application/problem+json` | # Create Custom Model > POST /v1/custom-model - create a custom model with an initial set of phrases. Request, response, and errors. Create a new custom model with an initial set of phrases. See the [Custom Models guide](/speech-to-text/features/custom-models/) for concepts. ```plaintext POST https://api.reson8.dev/v1/custom-model ``` ## Request [Section titled “Request”](#request) ### Headers [Section titled “Headers”](#headers) | Header | Value | | ------------- | --------------------------------------------- | | Authorization | `ApiKey ` or `Bearer ` | | Content-Type | `application/json` | See [Authentication](/authentication/) for which header to use in different situations. ### Body [Section titled “Body”](#body) | Field | Type | Required | Description | | ------------- | --------- | -------- | ---------------------------------------------------------------- | | `name` | string | Yes | Name of the custom model | | `description` | string | Yes | Description of the custom model | | `phrases` | string\[] | Yes | List of phrases (must not be empty, each at most 255 characters) | ### Example [Section titled “Example”](#example) * curl ```bash curl -X POST "https://api.reson8.dev/v1/custom-model" \ -H "Authorization: ApiKey " \ -H "Content-Type: application/json" \ -d '{ "name": "Cardiology", "description": "Cardiology-specific terminology", "phrases": ["myocardial infarction", "atrial fibrillation", "echocardiogram"] }' ``` * Python ```python import requests response = requests.post( "https://api.reson8.dev/v1/custom-model", headers={ "Authorization": "ApiKey ", "Content-Type": "application/json", }, json={ "name": "Cardiology", "description": "Cardiology-specific terminology", "phrases": ["myocardial infarction", "atrial fibrillation", "echocardiogram"], }, ) custom_model = response.json() ``` ## Response [Section titled “Response”](#response) `201 Created` | Field | Type | Description | | ------------- | ------ | ------------------------------------- | | `id` | string | Unique identifier of the custom model | | `name` | string | Name of the custom model | | `description` | string | Description of the custom model | | `phraseCount` | number | Number of phrases in the custom model | ### Example [Section titled “Example”](#example-1) ```json { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "Cardiology", "description": "Cardiology-specific terminology", "phraseCount": 3 } ``` ## Errors [Section titled “Errors”](#errors) | Status | Description | | ------------------------- | ------------------------------ | | 400 Bad Request | Missing or invalid fields | | 401 Unauthorized | Missing or invalid credentials | | 500 Internal Server Error | Unexpected server error | # Delete Custom Model > DELETE /v1/custom-model/{id} - permanently delete a custom model and all its phrases. Permanently delete a custom model and every phrase it contains. See the [Custom Models guide](/speech-to-text/features/custom-models/) for how models are used. ```plaintext DELETE https://api.reson8.dev/v1/custom-model/{id} ``` This cannot be undone Deleting a custom model removes all of its phrases. Transcription requests that reference the deleted model afterwards are rejected as an unknown custom model, so update any client still sending its ID first. ## Request [Section titled “Request”](#request) ### Headers [Section titled “Headers”](#headers) | Header | Value | | ------------- | --------------------------------------------- | | Authorization | `ApiKey ` or `Bearer ` | See [Authentication](/authentication/) for which header to use in different situations. ### Path Parameters [Section titled “Path Parameters”](#path-parameters) | Parameter | Type | Description | | --------- | ------ | ---------------------- | | `id` | string | ID of the custom model | ### Example [Section titled “Example”](#example) * curl ```bash curl -X DELETE "https://api.reson8.dev/v1/custom-model/a1b2c3d4-e5f6-7890-abcd-ef1234567890" \ -H "Authorization: ApiKey " ``` * Python ```python import requests response = requests.delete( "https://api.reson8.dev/v1/custom-model/a1b2c3d4-e5f6-7890-abcd-ef1234567890", headers={"Authorization": "ApiKey "}, ) response.raise_for_status() ``` ## Response [Section titled “Response”](#response) `204 No Content` The response has no body. ## Errors [Section titled “Errors”](#errors) | Status | Description | | ------------------------- | ------------------------------ | | 401 Unauthorized | Missing or invalid credentials | | 404 Not Found | Custom model not found | | 500 Internal Server Error | Unexpected server error | # Get Custom Model > GET /v1/custom-model/{id} - fetch a single custom model by ID. Get a single custom model by ID. Concepts are covered in the [Custom Models guide](/speech-to-text/features/custom-models/). ```plaintext GET https://api.reson8.dev/v1/custom-model/{id} ``` ## Request [Section titled “Request”](#request) ### Headers [Section titled “Headers”](#headers) | Header | Value | | ------------- | --------------------------------------------- | | Authorization | `ApiKey ` or `Bearer ` | See [Authentication](/authentication/) for which header to use in different situations. ### Path Parameters [Section titled “Path Parameters”](#path-parameters) | Parameter | Type | Description | | --------- | ------ | ---------------------- | | `id` | string | ID of the custom model | ### Example [Section titled “Example”](#example) * curl ```bash curl "https://api.reson8.dev/v1/custom-model/a1b2c3d4-e5f6-7890-abcd-ef1234567890" \ -H "Authorization: ApiKey " ``` * Python ```python import requests response = requests.get( "https://api.reson8.dev/v1/custom-model/a1b2c3d4-e5f6-7890-abcd-ef1234567890", headers={"Authorization": "ApiKey "}, ) custom_model = response.json() ``` ## Response [Section titled “Response”](#response) `200 OK` | Field | Type | Description | | ------------- | ------ | ------------------------------------- | | `id` | string | Unique identifier of the custom model | | `name` | string | Name of the custom model | | `description` | string | Description of the custom model | | `phraseCount` | number | Number of phrases in the custom model | ### Example [Section titled “Example”](#example-1) ```json { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "Cardiology", "description": "Cardiology-specific terminology", "phraseCount": 3 } ``` ## Errors [Section titled “Errors”](#errors) | Status | Description | | ------------------------- | ------------------------------ | | 401 Unauthorized | Missing or invalid credentials | | 404 Not Found | Custom model not found | | 500 Internal Server Error | Unexpected server error | # List Custom Models > GET /v1/custom-model - list all custom models for the authenticated organization. List all custom models for the authenticated organization. For background, see the [Custom Models guide](/speech-to-text/features/custom-models/). ```plaintext GET https://api.reson8.dev/v1/custom-model ``` ## Request [Section titled “Request”](#request) ### Headers [Section titled “Headers”](#headers) | Header | Value | | ------------- | --------------------------------------------- | | Authorization | `ApiKey ` or `Bearer ` | See [Authentication](/authentication/) for which header to use in different situations. ### Example [Section titled “Example”](#example) * curl ```bash curl "https://api.reson8.dev/v1/custom-model" \ -H "Authorization: ApiKey " ``` * Python ```python import requests response = requests.get( "https://api.reson8.dev/v1/custom-model", headers={"Authorization": "ApiKey "}, ) custom_models = response.json() ``` ## Response [Section titled “Response”](#response) `200 OK` The response is a JSON array of custom models. | Field | Type | Description | | ---------------- | ------ | ------------------------------------- | | `[].id` | string | Unique identifier of the custom model | | `[].name` | string | Name of the custom model | | `[].description` | string | Description of the custom model | | `[].phraseCount` | number | Number of phrases in the custom model | ### Example [Section titled “Example”](#example-1) ```json [ { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "Cardiology", "description": "Cardiology-specific terminology", "phraseCount": 3 } ] ``` ## Errors [Section titled “Errors”](#errors) | Status | Description | | ------------------------- | ------------------------------ | | 401 Unauthorized | Missing or invalid credentials | | 500 Internal Server Error | Unexpected server error | # Add Phrases > POST /v1/custom-model/{id}/phrases - add phrases to a custom model. Idempotent: existing phrases are reported as skipped. Add phrases to a custom model. Adding is idempotent: phrases already present are left unchanged and reported as skipped. See the [Custom Models guide](/speech-to-text/features/custom-models/) for guidance on choosing phrases. ```plaintext POST https://api.reson8.dev/v1/custom-model/{id}/phrases ``` ## Request [Section titled “Request”](#request) ### Headers [Section titled “Headers”](#headers) | Header | Value | | ------------- | --------------------------------------------- | | Authorization | `ApiKey ` or `Bearer ` | | Content-Type | `application/json` | See [Authentication](/authentication/) for which header to use in different situations. ### Path Parameters [Section titled “Path Parameters”](#path-parameters) | Parameter | Type | Description | | --------- | ------ | ---------------------- | | `id` | string | ID of the custom model | ### Body [Section titled “Body”](#body) | Field | Type | Required | Description | | --------- | --------- | -------- | --------------------------------------------------------------- | | `phrases` | string\[] | Yes | Phrases to add (must not be empty, each at most 255 characters) | ### Example [Section titled “Example”](#example) * curl ```bash curl -X POST "https://api.reson8.dev/v1/custom-model/a1b2c3d4-e5f6-7890-abcd-ef1234567890/phrases" \ -H "Authorization: ApiKey " \ -H "Content-Type: application/json" \ -d '{ "phrases": ["pericarditis", "atrial fibrillation"] }' ``` * Python ```python import requests response = requests.post( "https://api.reson8.dev/v1/custom-model/a1b2c3d4-e5f6-7890-abcd-ef1234567890/phrases", headers={ "Authorization": "ApiKey ", "Content-Type": "application/json", }, json={"phrases": ["pericarditis", "atrial fibrillation"]}, ) result = response.json() ``` ## Response [Section titled “Response”](#response) `200 OK` | Field | Type | Description | | --------- | ------ | -------------------------------------------------------------------- | | `added` | number | Number of phrases newly added | | `skipped` | number | Number of submitted phrases not added (already present or duplicate) | ### Example [Section titled “Example”](#example-1) In the request above `atrial fibrillation` was already in the model: ```json { "added": 1, "skipped": 1 } ``` ## Errors [Section titled “Errors”](#errors) | Status | Description | | ------------------------- | ---------------------------------------------------------------------- | | 400 Bad Request | Empty list, blank phrase, phrase is too long, or phrase limit exceeded | | 401 Unauthorized | Missing or invalid credentials | | 404 Not Found | Custom model not found | | 500 Internal Server Error | Unexpected server error | # Delete Phrases > POST /v1/custom-model/{id}/phrases/delete - remove phrases from a custom model by value. Remove phrases from a custom model by value. Removing is idempotent: phrases that are not present are reported as skipped. The [Custom Models guide](/speech-to-text/features/custom-models/) covers how phrases are used. ```plaintext POST https://api.reson8.dev/v1/custom-model/{id}/phrases/delete ``` ## Request [Section titled “Request”](#request) ### Headers [Section titled “Headers”](#headers) | Header | Value | | ------------- | --------------------------------------------- | | Authorization | `ApiKey ` or `Bearer ` | | Content-Type | `application/json` | See [Authentication](/authentication/) for which header to use in different situations. ### Path Parameters [Section titled “Path Parameters”](#path-parameters) | Parameter | Type | Description | | --------- | ------ | ---------------------- | | `id` | string | ID of the custom model | ### Body [Section titled “Body”](#body) | Field | Type | Required | Description | | --------- | --------- | -------- | ------------------------------------- | | `phrases` | string\[] | Yes | Phrases to remove (must not be empty) | ### Example [Section titled “Example”](#example) * curl ```bash curl -X POST "https://api.reson8.dev/v1/custom-model/a1b2c3d4-e5f6-7890-abcd-ef1234567890/phrases/delete" \ -H "Authorization: ApiKey " \ -H "Content-Type: application/json" \ -d '{ "phrases": ["pericarditis", "tachycardia"] }' ``` * Python ```python import requests response = requests.post( "https://api.reson8.dev/v1/custom-model/a1b2c3d4-e5f6-7890-abcd-ef1234567890/phrases/delete", headers={ "Authorization": "ApiKey ", "Content-Type": "application/json", }, json={"phrases": ["pericarditis", "tachycardia"]}, ) result = response.json() ``` ## Response [Section titled “Response”](#response) `200 OK` | Field | Type | Description | | --------- | ------ | ------------------------------------------------------------------ | | `deleted` | number | Number of phrases removed | | `skipped` | number | Number of submitted phrases not removed (not present or duplicate) | ### Example [Section titled “Example”](#example-1) In the request above `tachycardia` was not in the model: ```json { "deleted": 1, "skipped": 1 } ``` ## Errors [Section titled “Errors”](#errors) | Status | Description | | ------------------------- | ----------------------------------------------- | | 400 Bad Request | Empty list, blank phrase, or phrase is too long | | 401 Unauthorized | Missing or invalid credentials | | 404 Not Found | Custom model not found | | 500 Internal Server Error | Unexpected server error | # List Phrases > GET /v1/custom-model/{id}/phrases - list the phrases in a custom model, with pagination. List the phrases in a custom model. Results are paginated. See the [Custom Models guide](/speech-to-text/features/custom-models/) for how phrases affect transcription. ```plaintext GET https://api.reson8.dev/v1/custom-model/{id}/phrases ``` ## Request [Section titled “Request”](#request) ### Headers [Section titled “Headers”](#headers) | Header | Value | | ------------- | --------------------------------------------- | | Authorization | `ApiKey ` or `Bearer ` | See [Authentication](/authentication/) for which header to use in different situations. ### Path Parameters [Section titled “Path Parameters”](#path-parameters) | Parameter | Type | Description | | --------- | ------ | ---------------------- | | `id` | string | ID of the custom model | ### Query Parameters [Section titled “Query Parameters”](#query-parameters) | Parameter | Type | Default | Description | | --------- | ------ | ------- | ------------------------------- | | `page` | number | `0` | Zero-based page index | | `size` | number | `100` | Page size (between 1 and 10000) | ### Example [Section titled “Example”](#example) * curl ```bash curl "https://api.reson8.dev/v1/custom-model/a1b2c3d4-e5f6-7890-abcd-ef1234567890/phrases?page=0&size=100" \ -H "Authorization: ApiKey " ``` * Python ```python import requests response = requests.get( "https://api.reson8.dev/v1/custom-model/a1b2c3d4-e5f6-7890-abcd-ef1234567890/phrases", headers={"Authorization": "ApiKey "}, params={"page": 0, "size": 100}, ) phrases = response.json() ``` ## Response [Section titled “Response”](#response) `200 OK` | Field | Type | Description | | ----------------- | --------- | ------------------------------------------- | | `phrases` | object\[] | Phrases on this page | | `phrases[].value` | string | The phrase text | | `page` | number | Zero-based index of the returned page | | `size` | number | Page size used | | `total` | number | Total number of phrases in the custom model | ### Example [Section titled “Example”](#example-1) ```json { "phrases": [ { "value": "myocardial infarction" }, { "value": "atrial fibrillation" }, { "value": "echocardiogram" } ], "page": 0, "size": 100, "total": 3 } ``` ## Errors [Section titled “Errors”](#errors) | Status | Description | | ------------------------- | ------------------------------ | | 400 Bad Request | Invalid pagination parameters | | 401 Unauthorized | Missing or invalid credentials | | 404 Not Found | Custom model not found | | 500 Internal Server Error | Unexpected server error | # Prerecorded > POST /v1/speech-to-text/prerecorded - transcribe a complete audio file. Headers, query parameters, response fields, and error codes. Transcribe a complete audio file. See the [Prerecorded guide](/speech-to-text/prerecorded/) for concepts. ```plaintext POST https://api.reson8.dev/v1/speech-to-text/prerecorded ``` ## Request [Section titled “Request”](#request) ### Headers [Section titled “Headers”](#headers) | Header | Value | | ------------- | --------------------------------------------- | | Authorization | `ApiKey ` or `Bearer ` | | Content-Type | `application/octet-stream` | See [Authentication](/authentication/) for which header to use in different situations. ### Query Parameters [Section titled “Query Parameters”](#query-parameters) | Parameter | Type | Default | Description | | -------------------- | ------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `encoding` | string | `auto` | Audio encoding: `auto` for detected container formats, `m4a`, `m4v`, `mp4`, `mov`, `3gp`, or `3g2` for explicit seekable media containers, `pcm_s16le` for raw PCM, or `mulaw` / `alaw` for raw G.711 telephony audio. See [Audio Formats](/speech-to-text/features/audio-formats/) | | `sample_rate` | number | `16000` | Sample rate in Hz (only used depending on encoding) | | `channels` | number | `1` | Number of audio channels, 1-10 (only used depending on encoding) | | `language` | string | | Language to transcribe. Recommended for best quality. A comma-separated list (e.g. `nl,en`) constrains per-utterance auto-detection to those candidates. When omitted, the server auto-detects each utterance independently. See [Languages](/speech-to-text/features/languages/) for supported codes | | `phrases` | string | | Comma-separated phrases to bias transcription toward, up to 250. See [Custom Models](/speech-to-text/features/custom-models/) | | `custom_model_id` | string | | ID of a [custom model](/api/custom-model/create/) to bias transcription. Overrides the model configured on the API client | | `bias_strength` | number | `0.45` | Strength of contextual biasing. Must be a non-negative number | | `include_timestamps` | boolean | `false` | Include `start_ms` and `duration_ms` on transcripts and words | | `include_words` | boolean | `false` | Include word-level detail on transcripts | | `include_language` | boolean | `false` | Include the detected `language` on the response | | `include_confidence` | boolean | `false` | Include `confidence` on words | | `diarize` | boolean | `false` | Enable [speaker diarization](/speech-to-text/features/diarization/). Splits the response into per-speaker `segments` | | `max_speakers` | number | | Maximum number of distinct speakers (1-8). When omitted, the count is determined automatically. Only used when `diarize=true` | | `patterns` | string | | Comma-separated regex-style patterns for short alphanumeric tokens (order codes, licence plates) to recover. Only set when the token is likely present; cannot be combined with `phrases` or a custom model - see [Patterns](/speech-to-text/features/patterns/) | | `filler_mode` | string | `natural` | Controls filler words in the transcript: `clean` removes them, `natural` lets the model decide, and `verbatim` preserves them | ### Example [Section titled “Example”](#example) * curl ```bash curl -X POST "https://api.reson8.dev/v1/speech-to-text/prerecorded" \ -H "Authorization: ApiKey " \ -H "Content-Type: application/octet-stream" \ --data-binary @recording.wav ``` * Python ```python import requests with open("recording.wav", "rb") as f: response = requests.post( "https://api.reson8.dev/v1/speech-to-text/prerecorded", headers={ "Authorization": "ApiKey ", "Content-Type": "application/octet-stream", }, data=f, ) transcript = response.json() ``` ## Response [Section titled “Response”](#response) `200 OK` ### Fields [Section titled “Fields”](#fields) | Field | Type | Included | Description | | ------------- | ------ | ------------------------------------------------ | ----------------------------------------------------------------------------------------- | | `text` | string | Always | Full transcript of the audio file | | `language` | string | When `include_language=true` and `diarize=false` | The detected language code. When `diarize=true`, language moves into each segment instead | | `start_ms` | number | When `include_timestamps=true` | Start time in milliseconds | | `duration_ms` | number | When `include_timestamps=true` | Duration in milliseconds | | `words` | array | When `include_words=true` and `diarize=false` | Word-level detail. When `diarize=true`, word detail moves into each segment instead | | `segments` | array | When `diarize=true` | Per-speaker segments. | Each segment contains: | Field | Type | Included | Description | | ------------- | ------ | ------------------------------ | --------------------------------------------------- | | `text` | string | Always | Text spoken in this segment | | `language` | string | When `include_language=true` | The detected language code for this segment | | `speaker_id` | number | Always | Speaker label for this segment (integer, 0-indexed) | | `start_ms` | number | When `include_timestamps=true` | Start time in milliseconds | | `duration_ms` | number | When `include_timestamps=true` | Duration in milliseconds | | `words` | array | When `include_words=true` | Word-level detail for this segment | Each word contains: | Field | Type | Included | Description | | ------------- | ------ | ------------------------------ | -------------------------- | | `text` | string | Always | The recognized word | | `start_ms` | number | When `include_timestamps=true` | Start time in milliseconds | | `duration_ms` | number | When `include_timestamps=true` | Duration in milliseconds | | `confidence` | number | When `include_confidence=true` | Probability in `(0, 1]` | ### Example [Section titled “Example”](#example-1) * Default ```json { "text": "the patient presented with chest pain and shortness of breath" } ``` * With Diarization With `diarize=true`, the response is split into per-speaker `segments`. ```json { "text": "where does it hurt my chest mostly and for how long about two days", "segments": [ { "text": "where does it hurt", "speaker_id": 0 }, { "text": "my chest mostly", "speaker_id": 1 }, { "text": "and for how long", "speaker_id": 0 }, { "text": "about two days", "speaker_id": 1 } ] } ``` * Everything Included ```json { "text": "where does it hurt my chest mostly", "start_ms": 0, "duration_ms": 3200, "segments": [ { "text": "where does it hurt", "language": "en", "speaker_id": 0, "start_ms": 0, "duration_ms": 1500, "words": [ { "text": "where", "start_ms": 0, "duration_ms": 250, "confidence": 0.990 }, { "text": "does", "start_ms": 260, "duration_ms": 200, "confidence": 0.980 }, { "text": "it", "start_ms": 470, "duration_ms": 150, "confidence": 0.990 }, { "text": "hurt", "start_ms": 630, "duration_ms": 870, "confidence": 0.970 } ] }, { "text": "my chest mostly", "language": "en", "speaker_id": 1, "start_ms": 1700, "duration_ms": 1500, "words": [ { "text": "my", "start_ms": 1700, "duration_ms": 180, "confidence": 0.990 }, { "text": "chest", "start_ms": 1890, "duration_ms": 400, "confidence": 0.960 }, { "text": "mostly", "start_ms": 2300, "duration_ms": 900, "confidence": 0.980 } ] } ] } ``` ## Errors [Section titled “Errors”](#errors) Errors are returned as `application/problem+json` (RFC 7807) with a lowercase `code` field. | Status | Code | Description | | ------------------------- | ------------------------- | --------------------------------------------------------------------- | | 400 Bad Request | `invalid_audio` | Undecodable audio | | 400 Bad Request | `invalid_query_parameter` | Invalid query parameter | | 400 Bad Request | `session_rejected` | Unknown `custom_model_id`, or `patterns` combined with a custom model | | 401 Unauthorized | `unauthorized` | Missing or invalid credentials | | 402 Payment Required | `session_rejected` | Credit limit exceeded - see [Limits](/limits/) | | 413 Payload Too Large | | Request body exceeds the size limit (no `code` in the body) | | 429 Too Many Requests | `session_rejected` | Concurrent connection limit exceeded - see [Limits](/limits/) | | 500 Internal Server Error | `internal_server_error` | Unexpected server error | # Realtime > WSS /v1/speech-to-text/realtime - realtime WebSocket transcription. Query parameters, message formats, and errors. Real-time speech-to-text transcription over WebSocket. See the [Realtime guide](/speech-to-text/realtime/) for concepts. ```plaintext wss://api.reson8.dev/v1/speech-to-text/realtime ``` ## Request [Section titled “Request”](#request) ### Headers [Section titled “Headers”](#headers) | Header | Value | | ---------------------- | --------------------------------------------- | | Authorization | `ApiKey ` or `Bearer ` | | Sec-WebSocket-Protocol | `bearer, ` | See [Authentication](/authentication/) for which header to use in different situations. ### Query Parameters [Section titled “Query Parameters”](#query-parameters) | Parameter | Type | Default | Description | | -------------------- | ------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `encoding` | string | `auto` | Audio encoding: `auto` for detected container formats, `pcm_s16le` for raw PCM, or `mulaw` / `alaw` for raw G.711 telephony audio. See [Audio Formats](/speech-to-text/features/audio-formats/) | | `sample_rate` | number | `16000` | Sample rate in Hz (only used depending on encoding) | | `channels` | number | `1` | Number of audio channels, 1-10 (only used depending on encoding) | | `language` | string | | Language to transcribe. Recommended for best quality. A comma-separated list (e.g. `nl,en`) constrains per-utterance auto-detection to those candidates. When omitted, the server auto-detects each utterance independently. See [Languages](/speech-to-text/features/languages/) for supported codes | | `phrases` | string | | Comma-separated phrases to bias transcription toward, up to 250. See [Custom Models](/speech-to-text/features/custom-models/) | | `custom_model_id` | string | | ID of a [custom model](/api/custom-model/create/) to bias transcription. Overrides the model configured on the API client | | `bias_strength` | number | `0.45` | Strength of contextual biasing. Must be a non-negative number | | `include_timestamps` | boolean | `false` | Include `start_ms` and `duration_ms` on transcripts and words | | `include_words` | boolean | `false` | Include word-level detail on transcripts | | `include_language` | boolean | `false` | Include the detected `language` on transcripts (empty on interim) | | `include_confidence` | boolean | `false` | Include `confidence` on words | | `include_interim` | boolean | `false` | Include interim (partial) results and the `is_final` flag | | `diarize` | boolean | `false` | Enable [speaker diarization](/speech-to-text/features/diarization/). Adds `speaker_id` to each transcript | | `max_speakers` | number | | Maximum number of distinct speakers (1-8). When omitted, the count is determined automatically. Only used when `diarize=true` | | `patterns` | string | | Comma-separated regex-style patterns for short alphanumeric tokens (order codes, licence plates) to recover. Only set when the token is likely present; cannot be combined with `phrases` or a custom model - see [Patterns](/speech-to-text/features/patterns/) | | `filler_mode` | string | `natural` | Controls filler words in final transcripts: `clean` removes them, `natural` lets the model decide, and `verbatim` preserves them. Interim transcripts are unaffected | ### Example [Section titled “Example”](#example) * Python ```python import asyncio import json import websockets async def transcribe(): url = "wss://api.reson8.dev/v1/speech-to-text/realtime" headers = {"Authorization": "ApiKey "} async with websockets.connect(url, additional_headers=headers) as ws: async def send_audio(): try: with open("recording.wav", "rb") as f: while chunk := f.read(8192): await ws.send(chunk) await ws.send(json.dumps({"type": "flush_request"})) except Exception: await ws.close() raise sender = asyncio.create_task(send_audio()) async for message in ws: event = json.loads(message) print(event) if event["type"] == "flush_confirmation": break await sender asyncio.run(transcribe()) ``` * JavaScript (Browser) ```javascript const token = ""; const url = "wss://api.reson8.dev/v1/speech-to-text/realtime"; // Passes token via Sec-WebSocket-Protocol header const ws = new WebSocket(url, ["bearer", token]); ws.onopen = async () => { const audio = await (await fetch("/recording.wav")).arrayBuffer(); for (let i = 0; i < audio.byteLength; i += 8192) { ws.send(audio.slice(i, i + 8192)); } ws.send(JSON.stringify({ type: "flush_request" })); }; ws.onmessage = (event) => { const message = JSON.parse(event.data); console.log(message); if (message.type === "flush_confirmation") ws.close(); }; ``` ## Sending Messages [Section titled “Sending Messages”](#sending-messages) ### Audio [Section titled “Audio”](#audio) Binary WebSocket frame containing audio data. ### Flush Request [Section titled “Flush Request”](#flush-request) Force the server to finalize any buffered audio and return a transcript. ```json { "type": "flush_request", "id": "abc123" } ``` | Field | Type | Required | Description | | ----- | ------ | -------- | --------------------------------------------------------------------- | | `id` | string | No | Optional identifier, returned in the corresponding flush confirmation | ## Receiving Messages [Section titled “Receiving Messages”](#receiving-messages) ### Transcript [Section titled “Transcript”](#transcript) Returned when speech is recognized. By default, only final results are returned. When `include_interim=true`, interim (partial) results are also returned - these may change as more audio is processed. * Default ```json { "type": "transcript", "text": "the patient presented with chest pain" } ``` * With Diarization With `diarize=true`, each transcript carries the `speaker_id` of its dominant speaker. ```json { "type": "transcript", "text": "where does it hurt", "speaker_id": 0 } { "type": "transcript", "text": "my chest, mostly", "speaker_id": 1 } ``` * Everything Included ```json { "type": "transcript", "text": "the patient presented with chest pain", "language": "en", "is_final": true, "start_ms": 1200, "duration_ms": 2400, "speaker_id": 0, "words": [ { "text": "the", "start_ms": 1200, "duration_ms": 200, "confidence": 0.990 }, { "text": "patient", "start_ms": 1410, "duration_ms": 450, "confidence": 0.980 }, { "text": "presented", "start_ms": 1880, "duration_ms": 500, "confidence": 0.970 }, { "text": "with", "start_ms": 2400, "duration_ms": 200, "confidence": 0.990 }, { "text": "chest", "start_ms": 2620, "duration_ms": 350, "confidence": 0.960 }, { "text": "pain", "start_ms": 3000, "duration_ms": 600, "confidence": 0.970 } ] } ``` | Field | Type | Included | Description | | ------------- | ------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------- | | `text` | string | Always | The recognized text | | `language` | string | When `include_language=true` | The detected language code (empty on interim) | | `is_final` | boolean | When `include_interim=true` | `true` for final, `false` for interim | | `start_ms` | number | When `include_timestamps=true` | Start time in milliseconds | | `duration_ms` | number | When `include_timestamps=true` | Duration in milliseconds | | `speaker_id` | number | When `diarize=true` | Speaker label for this transcript (integer, 0-indexed). See [Diarization](/speech-to-text/features/diarization/) | | `words` | array | When `include_words=true` | Word-level detail | Each word contains: | Field | Type | Included | Description | | ------------- | ------ | ------------------------------ | -------------------------- | | `text` | string | Always | The recognized word | | `start_ms` | number | When `include_timestamps=true` | Start time in milliseconds | | `duration_ms` | number | When `include_timestamps=true` | Duration in milliseconds | | `confidence` | number | When `include_confidence=true` | Probability in `(0, 1]` | ### Flush Confirmation [Section titled “Flush Confirmation”](#flush-confirmation) Sent after a flush request has been processed. The `id` field is always present; it is `null` if no identifier was provided in the request. ```json { "type": "flush_confirmation", "id": "abc123" } ``` | Field | Type | Included | Description | | ----- | ------ | -------- | ----------------------------------------------------------------------------------- | | `id` | string | Always | The identifier from the corresponding flush request, or `null` if none was provided | ## Errors [Section titled “Errors”](#errors) Rejected WebSocket upgrades return no body; where available, the reason is in the `X-Error-Message` response header. | Status | Description | | --------------------- | ---------------------------------------------------------------------------------------------- | | 400 Bad Request | Invalid query parameter, unknown `custom_model_id`, or `patterns` combined with a custom model | | 401 Unauthorized | Missing or invalid credentials | | 402 Payment Required | Credit limit exceeded - see [Limits](/limits/) | | 429 Too Many Requests | Concurrent connection limit exceeded - see [Limits](/limits/) | ### Invalid Client Messages [Section titled “Invalid Client Messages”](#invalid-client-messages) Malformed or unrecognized client messages are ignored; the session continues. ### Close Codes [Section titled “Close Codes”](#close-codes) After the connection is established, errors are signalled through the WebSocket close code and reason (in browsers: `event.code` and `event.reason` on the `close` event). The reason is a stable machine-readable token: branch on the close code for coarse handling and on the reason for specifics. | Close code | Reason | Description | | ---------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | | 1000 | | Normal closure | | 1011 | `internal_error` | Unexpected server failure. Reconnect and resend unconfirmed audio | | 1011 | `internal_timeout` | An internal service did not recover within its reconnect budget (up to \~5 seconds). Reconnect and resend unconfirmed audio; back off if it recurs | | 4000 | `message_backlog` | Client did not read messages for 10 seconds. Consume promptly, then reconnect | A connection that drops without a close frame (surfaced as code 1006 in browsers) indicates a network failure or server crash. Treat it like 1011: reconnect and resend unconfirmed audio. # Turns > WSS /v1/speech-to-text/turns - turn-level WebSocket endpoint emitting speaker turn events. Query parameters, messages, and errors. Turn-level speech-to-text over WebSocket. The server emits events at turn boundaries. See the [Turns guide](/speech-to-text/turns/) for concepts. ```plaintext wss://api.reson8.dev/v1/speech-to-text/turns ``` ## Request [Section titled “Request”](#request) ### Headers [Section titled “Headers”](#headers) | Header | Value | | ---------------------- | --------------------------------------------- | | Authorization | `ApiKey ` or `Bearer ` | | Sec-WebSocket-Protocol | `bearer, ` | See [Authentication](/authentication/) for which header to use in different situations. ### Query Parameters [Section titled “Query Parameters”](#query-parameters) | Parameter | Type | Default | Description | | ------------------------ | ------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `encoding` | string | `auto` | Audio encoding: `auto` for detected container formats, `pcm_s16le` for raw PCM, or `mulaw` / `alaw` for raw G.711 telephony audio. See [Audio Formats](/speech-to-text/features/audio-formats/) | | `sample_rate` | number | `16000` | Sample rate in Hz (only used depending on encoding) | | `channels` | number | `1` | Number of audio channels, 1-10 (only used depending on encoding) | | `language` | string | | Language to transcribe. Recommended for best quality. A comma-separated list (e.g. `nl,en`) constrains per-utterance auto-detection to those candidates. When omitted, the server auto-detects each utterance independently. See [Languages](/speech-to-text/features/languages/) for supported codes | | `phrases` | string | | Comma-separated phrases to bias transcription toward, up to 250. See [Custom Models](/speech-to-text/features/custom-models/) | | `custom_model_id` | string | | ID of a [custom model](/api/custom-model/create/) to bias transcription. Overrides the model configured on the API client | | `bias_strength` | number | `0.45` | Strength of contextual biasing. Must be a non-negative number | | `include_timestamps` | boolean | `false` | Include `start_ms` and `duration_ms` on `turn_end_candidate` | | `include_words` | boolean | `false` | Include word-level detail on `turn_end_candidate` | | `include_language` | boolean | `false` | Include detected `language` on `turn_end_candidate` | | `include_confidence` | boolean | `false` | Include `confidence` on words | | `eager_turn_probability` | number | `0.5` | Emit an interim `turn_end_candidate` when the combined turn-end probability exceeds this threshold. Must be between `0` and `1` | | `final_turn_probability` | number | `0.92` | Emit the final `turn_end_candidate` and `turn_end` when the combined turn-end probability exceeds this threshold. Must be between `0` and `1` | | `patterns` | string | | Comma-separated regex-style patterns for short alphanumeric tokens (order codes, licence plates) to recover. Only set when the token is likely present; cannot be combined with `phrases` or a custom model - see [Patterns](/speech-to-text/features/patterns/) | | `filler_mode` | string | `natural` | Controls filler words in transcripts: `clean` removes them, `natural` lets the model decide, and `verbatim` preserves them | ### Example [Section titled “Example”](#example) * Python ```python import asyncio import json import websockets async def transcribe(): url = "wss://api.reson8.dev/v1/speech-to-text/turns" headers = {"Authorization": "ApiKey "} async with websockets.connect(url, additional_headers=headers) as ws: async def send_audio(): try: with open("recording.wav", "rb") as f: while chunk := f.read(8192): await ws.send(chunk) await ws.send(json.dumps({"type": "flush_request"})) except Exception: await ws.close() raise sender = asyncio.create_task(send_audio()) async for message in ws: event = json.loads(message) if event["type"] == "turn_start": print("-- turn start") elif event["type"] == "turn_end_candidate": print(event["text"]) elif event["type"] == "turn_end": print("-- turn end") if sender.done(): break await sender asyncio.run(transcribe()) ``` * JavaScript (Browser) ```javascript const token = ""; const url = "wss://api.reson8.dev/v1/speech-to-text/turns"; // Passes token via Sec-WebSocket-Protocol header const ws = new WebSocket(url, ["bearer", token]); ws.onopen = async () => { const audio = await (await fetch("/recording.wav")).arrayBuffer(); for (let i = 0; i < audio.byteLength; i += 8192) { ws.send(audio.slice(i, i + 8192)); } ws.send(JSON.stringify({ type: "flush_request" })); }; ws.onmessage = (event) => { const message = JSON.parse(event.data); if (message.type === "turn_start") console.log("-- turn start"); else if (message.type === "turn_end_candidate") console.log(message.text); else if (message.type === "turn_end") ws.close(); }; ``` ## Sending Messages [Section titled “Sending Messages”](#sending-messages) ### Audio [Section titled “Audio”](#audio) Binary WebSocket frame containing audio data. ### Flush Request [Section titled “Flush Request”](#flush-request) Force the current turn to finish immediately rather than waiting for turn-end detection. When a turn is active, the server emits a final `turn_end_candidate` followed by `turn_end`. ```json { "type": "flush_request" } ``` The request is a JSON text WebSocket frame and has no additional fields. ## Receiving Messages [Section titled “Receiving Messages”](#receiving-messages) ### Turn Start [Section titled “Turn Start”](#turn-start) Sent when the server detects the beginning of a new turn. ```json { "type": "turn_start" } ``` ### Turn End Candidate [Section titled “Turn End Candidate”](#turn-end-candidate) Sent when the server detects that a turn may be ending. Candidates can be emitted before the final `Turn End`; the final candidate contains the complete transcript. * Default ```json { "type": "turn_end_candidate", "text": "the patient presented with chest pain" } ``` * Everything Included ```json { "type": "turn_end_candidate", "text": "the patient presented with chest pain", "language": "en", "start_ms": 1200, "duration_ms": 2400, "words": [ { "text": "the", "start_ms": 1200, "duration_ms": 200, "confidence": 0.990 }, { "text": "patient", "start_ms": 1410, "duration_ms": 450, "confidence": 0.980 }, { "text": "presented", "start_ms": 1880, "duration_ms": 500, "confidence": 0.970 }, { "text": "with", "start_ms": 2400, "duration_ms": 200, "confidence": 0.990 }, { "text": "chest", "start_ms": 2620, "duration_ms": 350, "confidence": 0.960 }, { "text": "pain", "start_ms": 3000, "duration_ms": 600, "confidence": 0.970 } ] } ``` | Field | Type | Included | Description | | ------------- | ------ | ------------------------------ | ------------------------------------------------------------------------------- | | `text` | string | Always | The recognized text | | `language` | string | When `include_language=true` | The detected language code. Empty string when no language has been detected yet | | `start_ms` | number | When `include_timestamps=true` | Start time in milliseconds | | `duration_ms` | number | When `include_timestamps=true` | Duration in milliseconds | | `words` | array | When `include_words=true` | Word-level detail | Each word contains: | Field | Type | Included | Description | | ------------- | ------ | ------------------------------ | -------------------------- | | `text` | string | Always | The recognized word | | `start_ms` | number | When `include_timestamps=true` | Start time in milliseconds | | `duration_ms` | number | When `include_timestamps=true` | Duration in milliseconds | | `confidence` | number | When `include_confidence=true` | Probability in `(0, 1]` | ### Turn End [Section titled “Turn End”](#turn-end) Confirms that the previous turn end candidate is the final end for the turn. ```json { "type": "turn_end" } ``` ## Errors [Section titled “Errors”](#errors) Rejected WebSocket upgrades return no body; where available, the reason is in the `X-Error-Message` response header. | Status | Description | | --------------------- | ---------------------------------------------------------------------------------------------- | | 400 Bad Request | Invalid query parameter, unknown `custom_model_id`, or `patterns` combined with a custom model | | 401 Unauthorized | Missing or invalid credentials | | 402 Payment Required | Credit limit exceeded - see [Limits](/limits/) | | 429 Too Many Requests | Concurrent connection limit exceeded - see [Limits](/limits/) | ### Invalid Client Messages [Section titled “Invalid Client Messages”](#invalid-client-messages) Malformed or unrecognized client messages are ignored; the session continues. ### Close Codes [Section titled “Close Codes”](#close-codes) After the connection is established, errors are signalled through the WebSocket close code and reason (in browsers: `event.code` and `event.reason` on the `close` event). The reason is a stable machine-readable token: branch on the close code for coarse handling and on the reason for specifics. | Close code | Reason | Description | | ---------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | | 1000 | | Normal closure | | 1011 | `internal_error` | Unexpected server failure. Reconnect and resend unconfirmed audio | | 1011 | `internal_timeout` | An internal service did not recover within its reconnect budget (up to \~5 seconds). Reconnect and resend unconfirmed audio; back off if it recurs | | 4000 | `message_backlog` | Client did not read messages for 10 seconds. Consume promptly, then reconnect | A connection that drops without a close frame (surfaced as code 1006 in browsers) indicates a network failure or server crash. Treat it like 1011: reconnect and resend unconfirmed audio. # Authentication > How to authenticate Reson8 API requests with API keys, short-lived access tokens, and WebSocket sub-protocol headers. All API requests require authentication. There are three ways to authenticate: | Method | Scenario | | --------------------------------- | ----------------------------- | | API Key in `Authorization` header | Direct server connections | | Token in `Authorization` header | End-user client connections | | Token in sub-protocol header | Browser WebSocket connections | Caution API keys are confidential and must never be exposed externally, for example to end-users. Do not include them in client-side code, browser requests, or mobile apps. Use tokens instead. ## API Key [Section titled “API Key”](#api-key) The simplest way to authenticate server-to-server calls. Include your API key directly in the `Authorization` header - no token exchange required. ```plaintext Authorization: ApiKey ``` API keys are scoped to a client and can be created and managed from the [console](https://console.reson8.dev). ## Token [Section titled “Token”](#token) For client-side applications, use a short-lived access token instead of exposing your API key. Your server requests a token using the API key, then passes the token to the client. ```plaintext Authorization: Bearer ``` Access tokens are currently valid for 10 minutes. The lifetime is not configurable per request, so read the `expires_in` value from the token response to know when to request a new one. See the [token endpoint](/api/auth/token/) for how to obtain an access token. ## Token in Browser [Section titled “Token in Browser”](#token-in-browser) Browser WebSocket APIs do not support custom headers. As an alternative, pass the access token via the `Sec-WebSocket-Protocol` header: ```plaintext Sec-WebSocket-Protocol: bearer, ``` In JavaScript, the browser WebSocket API sets this header automatically: ```javascript const ws = new WebSocket(url, ["bearer", accessToken]); ``` # Choosing an Endpoint > Choose between the Reson8 prerecorded, realtime, and turns speech-to-text endpoints based on your use case. Reson8 offers three speech-to-text endpoints. Which one to use depends on where your audio comes from and what you want to do with the result. * **[Prerecorded](/speech-to-text/prerecorded/)** - Upload a complete audio file over REST and receive the full transcript in one response. * **[Realtime](/speech-to-text/realtime/)** - Stream live audio over WebSocket and receive transcripts as the speech is recognized. * **[Turns](/speech-to-text/turns/)** - Stream live audio over WebSocket and receive events when a speaker starts and stops talking, with the transcript of each turn. ## Common Use Cases [Section titled “Common Use Cases”](#common-use-cases) | Use case | Endpoint | | ----------------------------------------------------------------------- | ----------- | | Quickly transcribing a meeting or live stream after it has finished | Prerecorded | | Processing an audio archive | Prerecorded | | Showing live subtitles during a video call, webinar, or broadcast | Realtime | | Dictating notes or reports into your application, hands-free | Realtime | | Building a voice agent that responds the moment the user stops speaking | Turns | | Stopping a speaking voice agent when the user interrupts (barge-in) | Turns | ## How They Differ [Section titled “How They Differ”](#how-they-differ) | | Prerecorded | Realtime | Turns | | -------------------------------------------------------- | ------------------- | ----------------------------- | -------------------------------------- | | Protocol | REST (`POST`) | WebSocket | WebSocket | | Input | Complete audio file | Live audio stream | Live audio stream | | Output | Full transcript | Interim and final transcripts | Turn start/end events with transcripts | | [Diarization](/speech-to-text/features/diarization/) | Yes | Yes | No | | [Custom models](/speech-to-text/features/custom-models/) | Yes | Yes | Yes | # Glossary > Definitions of key Reson8 speech-to-text terms: access tokens, interim and final results, flush, phrase boosting, PCM audio, and more. Short definitions of terms used throughout the API and this documentation. | Term | Description | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Access token** | A short-lived bearer token obtained by exchanging an API key at `POST /v1/auth/token`. Used for client-side applications to avoid exposing the API key directly. | | **API key** | A secret token used to authenticate requests to the Reson8 API. Scoped to a single client. | | **Code switching** | Changing language during a session. Picked up between utterances with auto-detection or a language list, not within one. See [Code switching](/speech-to-text/features/languages/#code-switching). | | **Confidence score** | The model’s probability for a recognized word, in `(0, 1]`. Available when `include_confidence` is enabled. | | **Custom model** | A named collection of phrases that biases transcription toward domain-specific vocabulary. See [Custom Models](/speech-to-text/features/custom-models/). | | **Final result** | A stable transcription that will not change as more audio is received. Indicated by `is_final: true`. | | **Flush** | A request to force the server to finalize any buffered audio and return a transcript immediately. The server responds with a flush confirmation. | | **Interim result** | A partial transcription that may be revised as more audio is processed. Useful for live feedback during streaming. | | **Pattern** | A regex-style description of a short structured token, such as an order code or licence plate, to recover in transcription. See [Patterns](/speech-to-text/features/patterns/). | | **PCM** | Pulse-code modulation - a raw, uncompressed audio encoding. The `pcm_s16le` format (16-bit signed, little-endian) is supported when `encoding` is set explicitly. | | **Phrases** | Terms that bias transcription toward your vocabulary, passed per request via the `phrases` parameter or collected in a [custom model](/speech-to-text/features/custom-models/). | | **Segment** | A per-speaker piece of a diarized prerecorded response, with its own text and speaker ID. See [Diarization](/speech-to-text/features/diarization/). | | **Session** | An active WebSocket connection during which audio is streamed and transcriptions are returned. | | **Speaker ID** | An integer label for a speaker within a session, assigned in the order speakers are first heard. See [Diarization](/speech-to-text/features/diarization/). | | **Turn** | One speaker’s stretch of speech in a conversation, opened by `turn_start` and closed by `turn_end`. See [Turns](/speech-to-text/turns/). | | **Turn end candidate** | An early signal that a turn is ending, carrying the transcript so far. Each candidate replaces the previous one; `turn_end` confirms the last. See [Turns](/speech-to-text/turns/). | | **Utterance** | A continuous stretch of speech, ended by a pause. In realtime each final transcript covers one utterance; in turns, one turn. | # LiveKit Agents > Use Reson8 speech-to-text in LiveKit Agents voice AI applications with the livekit-plugins-reson8 Python package. Use Reson8 speech-to-text with [LiveKit Agents](https://docs.livekit.io/agents/) to build real-time voice AI applications. ## Installation [Section titled “Installation”](#installation) ```bash pip install livekit-plugins-reson8 ``` ## Quick start [Section titled “Quick start”](#quick-start) ```python from livekit.agents import AutoSubscribe, JobContext, WorkerOptions, cli from livekit.agents.voice import VoiceAgent from livekit.plugins import openai, reson8 async def entrypoint(ctx: JobContext): await ctx.connect(auto_subscribe=AutoSubscribe.AUDIO_ONLY) agent = VoiceAgent( instructions="You are a helpful assistant.", stt=reson8.STT(), # streaming + server-side turn detection, any language llm=openai.LLM(), # any LLM tts=openai.TTS(), # any TTS ) agent.start(ctx.room) await agent.say("Hallo, hoe kan ik je helpen?") if __name__ == "__main__": cli.run_app(WorkerOptions(entrypoint_fnc=entrypoint)) ``` `reson8.STT()` auto-detects the spoken language. Pass any language code to pin it, e.g. `reson8.STT(language="nl")` for Dutch. The LLM and TTS can be any provider supported by LiveKit, including models available through [LiveKit Inference](https://docs.livekit.io/agents/integrations/). ## How it works [Section titled “How it works”](#how-it-works) ```plaintext Microphone → Reson8 STT (streaming + turn detection) → LLM → TTS → Speaker ``` When LiveKit uses the plugin in a voice agent, `reson8.STT()` opens a streaming connection to Reson8’s [Turns endpoint](/speech-to-text/turns/). Reson8 detects conversational turn boundaries server-side - no separate VAD plugin is required: 1. **Preflight transcript** - a [turn end candidate](/speech-to-text/turns/): an eager guess that the turn is over, so your agent can start responding immediately. 2. **Final transcript** - the turn end, confirming the last candidate once the turn really is complete. 3. **Cancellation** - if the speaker keeps talking, the preflight is withdrawn and streaming continues. This server-side turn detection keeps voice-agent responses low-latency while avoiding premature interruptions when the user pauses mid-sentence. Upgrading from Silero VAD Earlier versions of this guide paired Reson8’s batch API with `livekit-plugins-silero` and a `StreamAdapter`. The current plugin streams with built-in turn detection, so you no longer need `livekit-plugins-silero`, `livekit-plugins-turn-detector`, or a `StreamAdapter` - just pass `reson8.STT()` directly as the agent’s `stt`. ## Configuration [Section titled “Configuration”](#configuration) Only `api_key` is required (can also be set via the `RESON8_API_KEY` environment variable). All other parameters are optional. | Parameter | Required | Default | Description | | -------------------- | -------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `api_key` | Yes | `RESON8_API_KEY` env var | API key from [console.reson8.dev](https://console.reson8.dev) | | `api_url` | No | `https://api.reson8.dev` | API base URL | | `language` | No | `None` (auto-detect) | One of the [supported languages](/speech-to-text/features/languages/) | | `custom_model_id` | No | `None` | Custom model ID for domain-specific transcription | | `sample_rate` | No | `16000` | Audio sample rate in Hz | | `encoding` | No | `pcm_s16le` | Audio encoding | | `channels` | No | `1` | Number of audio channels | | `include_timestamps` | No | `False` | Include timing data on transcripts | | `include_words` | No | `False` | Include word-level detail | | `include_confidence` | No | `False` | Include confidence scores. Only applies when LiveKit calls the plugin’s non-streaming `recognize()` method, which uses the prerecorded endpoint; streaming ignores it | | `include_language` | No | `False` | Report the detected language while streaming | Call `STT.update_options(...)` to change settings at runtime; active streaming sessions reconnect automatically to apply them. ## Environment variables [Section titled “Environment variables”](#environment-variables) ```bash RESON8_API_KEY=your-api-key LIVEKIT_URL=wss://your-project.livekit.cloud LIVEKIT_API_KEY=your-livekit-key LIVEKIT_API_SECRET=your-livekit-secret ``` ## Links [Section titled “Links”](#links) * [PyPI](https://pypi.org/project/livekit-plugins-reson8/) * [GitHub](https://github.com/reson8labs/livekit-plugins-reson8) * [LiveKit Agents documentation](https://docs.livekit.io/agents/) # Limits > Concurrency and credit limits for the Reson8 API: how concurrent connections are counted and how the free plan credit limit works. Two limits apply when starting a speech-to-text request: a concurrent connection limit and, on the free plan, a credit limit. Both are checked when the request starts - a request that has started always runs to completion. ## Concurrent Connections [Section titled “Concurrent Connections”](#concurrent-connections) Every active speech-to-text session counts as one connection: * A [Realtime](/speech-to-text/realtime/) or [Turns](/speech-to-text/turns/) WebSocket connection counts while it is open. * A [Prerecorded](/speech-to-text/prerecorded/) request counts while it is being processed. A connection stops counting the moment it ends. When you are at the limit, new requests are rejected with status `429`; sessions already in progress are unaffected. ## Credit Limit [Section titled “Credit Limit”](#credit-limit) On the free plan, usage is capped by a credit limit. As long as you are under the limit you can start any request, and it will run to completion. Once the limit is reached, new requests are rejected with status `402`. Paid plans have no credit limit. # Security & Compliance > How Reson8 handles your data: self-owned GPU infrastructure in the Netherlands, zero subprocessors, zero audio retention, and no training on customer data. Reson8 is built for regulated industries. Your data stays where you expect it to. ## Infrastructure [Section titled “Infrastructure”](#infrastructure) Reson8 owns and operates its own GPU stack in the Netherlands. All audio is processed on this infrastructure - there are zero subprocessors, so your audio data never passes through a third party. ## Data Handling [Section titled “Data Handling”](#data-handling) * **Zero audio retention** - audio is processed for transcription and not retained afterwards. * **No training on customer data** - your audio and transcripts are never used to train models. ## Compliance [Section titled “Compliance”](#compliance) Reson8 is fully ISO 27001 certified and GDPR compliant. Certifications and policies are available in the [Trust Center](https://trust.reson8.dev). For compliance questions, [contact us](/support/). # Audio Formats > Audio formats supported by the Reson8 speech-to-text APIs: automatic container detection, raw audio encodings, and streaming containers. All speech-to-text endpoints accept audio in two ways: a container format that the server detects automatically, or raw audio described explicitly with the `encoding`, `sample_rate`, and `channels` query parameters. ## Automatic Detection [Section titled “Automatic Detection”](#automatic-detection) With the default `encoding=auto`, the server detects the audio format by reading the container headers at the start of the data. Most common formats (WAV, M4A, MP3, OGG, FLAC, WebM, etc.) are supported. On the [Prerecorded](/speech-to-text/prerecorded/) endpoint, you can also set a seekable media container explicitly: `m4a`, `m4v`, `mp4`, `mov`, `3gp`, or `3g2`. ## Raw Audio [Section titled “Raw Audio”](#raw-audio) If you are sending raw audio without container headers, set the `encoding` parameter explicitly along with `sample_rate` and `channels`: | Encoding | Description | | ----------- | ----------------------------- | | `pcm_s16le` | Raw PCM, 16-bit little-endian | | `mulaw` | G.711 mu-law telephony audio | | `alaw` | G.711 A-law telephony audio | ## Streaming Containers [Section titled “Streaming Containers”](#streaming-containers) When streaming to the [Realtime](/speech-to-text/realtime/) or [Turns](/speech-to-text/turns/) endpoint, you can use a container with an indefinite length (e.g. a WAV header with the data size set to the maximum value) and continuously append audio frames. Recommended streaming setup If you control the capture pipeline, send raw PCM at 16 kHz mono: `encoding=pcm_s16le&sample_rate=16000&channels=1`. There are no container headers to lose, so the reconnecting caveat below does not apply. Reconnecting mid-stream If you reconnect to the WebSocket and resume sending audio, the server will not be able to detect the format because the container headers are missing. Each new connection must start with a fresh audio stream that includes the headers. Alternatively, set the `encoding` parameter explicitly to bypass format detection entirely. # Custom Models > Bias Reson8 transcription toward domain-specific terminology such as product names, technical terms, and jargon with custom models built from your own phrases. A custom model is a named collection of phrases that biases transcription toward domain-specific vocabulary, such as product names, technical terms, or internal jargon. Custom models are available on the [Prerecorded](/speech-to-text/prerecorded/), [Realtime](/speech-to-text/realtime/), and [Turns](/speech-to-text/turns/) APIs. ## Creating a Model [Section titled “Creating a Model”](#creating-a-model) There are two ways to create a custom model: * **Through the API**, by specifying the phrases yourself. See the API reference to [create a model](/api/custom-model/create/), [add](/api/custom-model/phrases/add/), [list](/api/custom-model/phrases/list/), and [delete phrases](/api/custom-model/phrases/delete/) as the vocabulary evolves, and to [delete the model](/api/custom-model/delete/) once you no longer need it. * **Through the console**, by providing a prompt, files, and some context about your use case. A model is then created for you based on that input. See the [console](https://console.reson8.dev). For example, creating a model with a few phrases: ```bash curl -X POST "https://api.reson8.dev/v1/custom-model" \ -H "Authorization: ApiKey " \ -H "Content-Type: application/json" \ -d '{ "name": "Cardiology", "description": "Cardiology terms and abbreviations", "phrases": ["myocardial infarction", "atrial fibrillation", "echocardiogram"] }' ``` Models created through the console also show up in the API, so they can be inspected and managed there like any other model. When possible, we recommend the API: it gives exact control over the phrase list and lets you manage it programmatically as your vocabulary evolves. There is no limit on the number of custom models you can create. ## Per-Request Phrases [Section titled “Per-Request Phrases”](#per-request-phrases) For a small number of phrases, you don’t need a custom model at all: pass them directly with the request via the `phrases` query parameter, available on all three APIs. This is limited to 250 phrases per request - use a custom model when the vocabulary is larger or reused across requests. ## Using a Model [Section titled “Using a Model”](#using-a-model) A custom model can be applied in two ways: * **Configured on an API client** in the [console](https://console.reson8.dev), so every request made with that client uses the model by default. * **Passed per request**, via the `custom_model_id` query parameter on the [Prerecorded](/speech-to-text/prerecorded/), [Realtime](/speech-to-text/realtime/), and [Turns](/speech-to-text/turns/) APIs: ```plaintext ?custom_model_id= ``` When both are set, the query parameter takes priority over the model configured on the API client. ### Tuning Biasing [Section titled “Tuning Biasing”](#tuning-biasing) The [Prerecorded](/api/speech-to-text/prerecorded/), [Realtime](/api/speech-to-text/realtime/), and [Turns](/api/speech-to-text/turns/) APIs accept an optional control: * `bias_strength` controls how strongly the selected phrases influence transcription. Its default is `0.45`; larger values apply stronger biasing. The default is suitable for most requests. Increase it only when the expected terminology is not being recovered, since stronger biasing can introduce irrelevant terms. ## Choosing Phrases [Section titled “Choosing Phrases”](#choosing-phrases) Good phrases are the terms the base model gets wrong: specialised vocabulary, uncommon names, brand and product names. Everyday words are already recognised well and only dilute the model. A phrase can be at most 255 characters long, and a model can currently hold at most 50,000 phrases; we expect to raise this limit over time. Keep phrases focused Biasing improves accuracy on audio that contains the phrases, but a large set of irrelevant ones can **degrade** transcription. For short structured tokens like order codes and licence plates, use [Patterns](/speech-to-text/features/patterns/) instead; they describe the shape of a token rather than a fixed phrase. Note that patterns and phrase biasing are mutually exclusive - `patterns` cannot be combined with `phrases` or a custom model. # Diarization > Enable speaker diarization to label Reson8 transcripts with speaker IDs - who spoke when - on prerecorded and realtime audio. Speaker diarization partitions transcribed speech by speaker - answering “who spoke when”. When enabled, each piece of transcribed text is labelled with a `speaker_id` so you can tell participants apart in a conversation. Diarization is available on the [Prerecorded](/speech-to-text/prerecorded/) and [Realtime](/speech-to-text/realtime/) APIs. It is not available on the [Turns](/speech-to-text/turns/) API. ## Enabling Diarization [Section titled “Enabling Diarization”](#enabling-diarization) Set the `diarize` query parameter to `true`. Optionally cap the number of distinct speakers with `max_speakers` (1-8); leave it unset to let the server determine the count automatically. ```plaintext ?diarize=true&max_speakers=2 ``` ## Speaker IDs [Section titled “Speaker IDs”](#speaker-ids) Each speaker is identified by an integer `speaker_id`, starting at `0`. IDs are assigned in the order speakers are first heard and remain stable for the duration of a session. Anonymous labels Speaker IDs are anonymous labels, not identities. `speaker_id: 0` is simply “the first speaker heard” - diarization does not recognise *who* a person is across sessions. ## In the Response [Section titled “In the Response”](#in-the-response) On prerecorded, the response is split into per-speaker `segments`, each with its own `speaker_id` and text. On realtime, each transcript message carries the `speaker_id` of its dominant speaker. See the API reference for [Prerecorded](/api/speech-to-text/prerecorded/) and [Realtime](/api/speech-to-text/realtime/) for the full response fields. # Languages > Languages supported by Reson8 speech-to-text, how per-utterance auto-detection works, and how code switching between languages is handled. Speech-to-text supports the same languages on the [Prerecorded](/speech-to-text/prerecorded/), [Realtime](/speech-to-text/realtime/), and [Turns](/speech-to-text/turns/) APIs. Pass the `language` query parameter whenever you know the language ahead of time - pinning the language produces better transcription quality than auto-detect. When omitted, the server auto-detects each utterance independently, which is less reliable for short utterances. ## Multiple languages [Section titled “Multiple languages”](#multiple-languages) The `language` parameter also accepts a comma-separated list of codes (e.g. `language=nl,en`). The server then auto-detects each utterance independently, restricted to the listed languages. This is more reliable than unrestricted auto-detection, but pinning a single language gives the best quality. ## Code switching [Section titled “Code switching”](#code-switching) The language is decided once per utterance: a final transcript in realtime, a turn in turns, or an internal chunk of up to 30 seconds in prerecorded. So speakers can switch language between utterances, but a switch within one is not detected, so the whole utterance is transcribed in its detected language. For sessions where switching is expected, pass the expected languages as a list (`language=nl,en`) rather than pinning one: a pinned language never switches, and unrestricted auto-detection is less reliable on short utterances. Set `include_language=true` to see which language each utterance was detected as. In prerecorded, the reported `language` reflects the start of the file. ## Supported languages [Section titled “Supported languages”](#supported-languages) | Code | Language | | ---- | ---------- | | `de` | German | | `en` | English | | `es` | Spanish | | `fr` | French | | `fy` | Frisian | | `it` | Italian | | `nl` | Dutch | | `pl` | Polish | | `pt` | Portuguese | | `sv` | Swedish | More languages are coming. # Patterns > Recover structured alphanumeric tokens - order codes, reference numbers, licence plates - in Reson8 transcripts using pattern syntax. Patterns help recover short, structured alphanumeric tokens - order codes, reference numbers, licence plates - that are easy to mishear when spoken letter-by-letter and digit-by-digit. When you know the shape of a token that may appear in the audio, describe it with a subset of regular-expression syntax and the transcription is corrected to fit. Patterns are available on the [Prerecorded](/speech-to-text/prerecorded/), [Realtime](/speech-to-text/realtime/), and [Turns](/speech-to-text/turns/) APIs. ## Using Patterns [Section titled “Using Patterns”](#using-patterns) Pass the `patterns` query parameter as a comma-separated list of patterns. Each one describes a single token to recover. ```plaintext ?patterns=AMZ[0-9]{6} ?patterns=[A-Z]{2}[0-9]{2} [A-Z]{3} ?patterns=(INV)?[0-9]{4,5} ?patterns=AMZ[0-9]{6},ORD[0-9]{5} ``` Patterns use a subset of regular-expression syntax: | Syntax | Meaning | Example | | -------- | -------------------------------------------- | ------------------- | | `[0-9]` | a digit | `[0-9]{6}` | | `[A-Z]` | a letter | `[A-Z]{3}` | | `{n}` | repeat the preceding class exactly `n` times | `[0-9]{6}` | | `{m,n}` | repeat the preceding class `m` to `n` times | `[0-9]{4,6}` | | `(...)?` | an optional group - matched only when spoken | `(INV)?[0-9]{5}` | | `AMZ` | literal letters/digits, e.g. a fixed prefix | `AMZ[0-9]{6}` | | ``or `-` | a separator | `[A-Z]{2} [0-9]{3}` | Optional groups and ranges are resolved against the audio: an optional prefix is added only when it is actually spoken, and a `{m,n}` range adopts the spoken length. The more specific the pattern, the better - a fixed prefix or known structure (`AMZ[0-9]{6}`) is recovered more reliably than an open one. A pattern matches tokens of at most 32 characters once fully expanded (the prefix and any digits or letters combined, separators excluded), and may describe at most 32 alternative shapes (each optional group and `{m,n}` range multiplies the alternatives). Syntax outside this subset - for example unbounded repeats like `[0-9]+` - and patterns exceeding either limit are rejected with a `400 Bad Request`. Patterns cannot be combined with phrase biasing: don’t set `patterns` together with `phrases` or `custom_model_id`, or on requests from an API client with a [custom model](/speech-to-text/features/custom-models/) configured. ## Examples [Section titled “Examples”](#examples) | Token type | Example value | Pattern | | ------------------------- | ----------------------- | --------------------------- | | Order code | `AMZ374019` | `AMZ[0-9]{6}` | | Reference number | `4429105538` | `[0-9]{10}` | | Licence plate | `RU77 TVG` | `[A-Z]{2}[0-9]{2} [A-Z]{3}` | | Invoice (optional prefix) | `INV4821` or `4821` | `(INV)?[0-9]{4}` | | Variable-length code | `AMZ3740` … `AMZ374019` | `AMZ[0-9]{4,6}` | Only set patterns you expect Patterns improve accuracy on audio that contains the token, but can **degrade** transcription of audio that does not. Set `patterns` only when the expected token is likely present in the request - don’t apply them globally. See the API reference for [Prerecorded](/api/speech-to-text/prerecorded/), [Realtime](/api/speech-to-text/realtime/), and [Turns](/api/speech-to-text/turns/). # Prerecorded > Upload a complete audio file to the Reson8 REST API and receive the full speech-to-text transcript in one response. The REST API provides speech-to-text transcription for complete audio files. You upload an audio file and receive the full transcript in the response. Use it when the audio already exists in full: recordings of finished meetings or calls, voice memos, or an audio archive. When in doubt, see [Choosing an Endpoint](/choosing-an-endpoint/). ## Audio Format [Section titled “Audio Format”](#audio-format) With the default `encoding=auto`, the server detects the audio format from the uploaded file; for raw audio, set `encoding` explicitly. See [Audio Formats](/speech-to-text/features/audio-formats/) for the supported formats. ## Language [Section titled “Language”](#language) The server auto-detects the language by default; use the `language` query parameter to pin one or constrain detection to a list. See [Languages](/speech-to-text/features/languages/). Set `include_language=true` to include the detected `language` in the response. ## Diarization [Section titled “Diarization”](#diarization) Pass `diarize=true` to label transcribed speech by speaker. The response is split into per-speaker `segments`. See [Diarization](/speech-to-text/features/diarization/). ## How It Works [Section titled “How It Works”](#how-it-works) 1. **Authenticate** - Include an [authentication](/authentication/) header in your request. 2. **Configure** - Use [query parameters](/api/speech-to-text/prerecorded/#query-parameters) to set the audio encoding and which fields to include in the response. 3. **Upload audio** - Send the audio file as the request body with `Content-Type: application/octet-stream`. 4. **Receive transcript** - The server processes the entire file and returns the transcript as JSON. See the [API reference](/api/speech-to-text/prerecorded/) for full details on fields and error codes, or [Getting Started](/) for a working curl example. # Realtime > Stream audio to the Reson8 realtime WebSocket API and receive live interim and final speech-to-text transcripts. The WebSocket API provides real-time speech-to-text transcription. You send audio data and receive transcript results as the speech is recognized. Use it when you need a live transcript while audio is being captured: live subtitles, dictation, or transcribing an ongoing call or meeting. When in doubt, see [Choosing an Endpoint](/choosing-an-endpoint/). ## Interim and Final Results [Section titled “Interim and Final Results”](#interim-and-final-results) By default the server sends only final transcripts, whose text will not change. Set `include_interim=true` to also receive interim transcripts: early results that may be revised as more audio arrives. Show interim text live for immediate feedback, but only persist it once a transcript with `is_final: true` arrives. ## Audio Format [Section titled “Audio Format”](#audio-format) With the default `encoding=auto`, the server detects the audio format from the container headers at the start of the stream; for raw audio, set `encoding` explicitly. See [Audio Formats](/speech-to-text/features/audio-formats/) for the supported formats, streaming containers, and the reconnecting caveat. ## Language [Section titled “Language”](#language) The server auto-detects the language by default; use the `language` query parameter to pin one or constrain detection to a list. See [Languages](/speech-to-text/features/languages/). Set `include_language=true` to include the detected `language` on transcripts (empty on interim transcripts, since they carry no language signal). ## Diarization [Section titled “Diarization”](#diarization) Pass `diarize=true` to label each transcript with the `speaker_id` of its dominant speaker. See [Diarization](/speech-to-text/features/diarization/). ## How It Works [Section titled “How It Works”](#how-it-works) 1. **Connect** - Open a WebSocket connection to `wss://api.reson8.dev/v1/speech-to-text/realtime` with an [authentication](/authentication/) header. 2. **Configure** - Use [query parameters](/api/speech-to-text/realtime/#query-parameters) to set the audio encoding and which fields to include in the response. 3. **Stream audio** - Send audio data as binary WebSocket frames. 4. **Receive transcripts** - The server returns transcript messages as JSON text frames. Results can be interim (partial, may change) or final (stable). 5. **Close** - Close the WebSocket connection when done. See the [API reference](/api/speech-to-text/realtime/) for full details on messages, fields, and error codes. ## Sequence Diagram [Section titled “Sequence Diagram”](#sequence-diagram) ``` sequenceDiagram Client->>Server: Create Connection (auth + query params) activate Server Client->>Server: Audio (binary) Client->>Server: Audio (binary) Server->>Client: Transcript (Partial) Client->>Server: Audio (binary) Server->>Client: Transcript (Final) Client->>Server: Audio (binary) Client->>Server: Flush Request Server->>Client: Transcript (Final) Server-->>Client: Flush Confirmation Client->>Server: Close Connection deactivate Server ``` ## Flush [Section titled “Flush”](#flush) Send `{"type":"flush_request"}` as a JSON text frame to force any buffered audio to be finalized immediately. The server returns the final transcript followed by a `flush_confirmation` - see the [API reference](/api/speech-to-text/realtime/#flush-request) for the message format. ## Ping / Pong [Section titled “Ping / Pong”](#ping--pong) The server sends WebSocket ping frames every 30 seconds to keep the connection alive. This uses the built-in WebSocket ping/pong mechanism - clients should respond with a pong for every ping received. Most WebSocket libraries and browsers handle this automatically. # Turns > Detect speaker turn starts and ends with the Reson8 turns WebSocket API - built for low-latency conversational voice agents. The WebSocket API provides turn-level speech-to-text. You send audio data and receive events at the start and end of each turn, along with the transcript of each turn. Use it for voice agents, where every millisecond of processing time counts: the turn end tells the agent to respond, and the turn start lets it stop talking when the user interrupts (barge-in). If you build voice agents with LiveKit, the [LiveKit integration](/integrations/livekit/) uses this endpoint for you. When in doubt, see [Choosing an Endpoint](/choosing-an-endpoint/). [Speaker diarization](/speech-to-text/features/diarization/) is not available on this endpoint. ## Turn Detection [Section titled “Turn Detection”](#turn-detection) A turn opens with a `turn_start` message when speech begins. As the model grows more confident that the turn is ending, it emits `turn_end_candidate` messages carrying the transcript so far - each new candidate replaces the previous one. A `turn_end` message confirms the last candidate as the final transcript of the turn. The `eager_turn_probability` [query parameter](/api/speech-to-text/turns/#query-parameters) controls how early candidates appear; `final_turn_probability` controls when the turn end is confirmed. End-of-turn detection is not based on silence alone: the model combines acoustic cues with the semantic content of what has been said. A thinking pause mid-sentence is less likely to end a turn than a pause after a completed thought. ## Audio Format [Section titled “Audio Format”](#audio-format) With the default `encoding=auto`, the server detects the audio format from the container headers at the start of the stream; for raw audio, set `encoding` explicitly. See [Audio Formats](/speech-to-text/features/audio-formats/) for the supported formats, streaming containers, and the reconnecting caveat. ## Language [Section titled “Language”](#language) The server auto-detects the language by default; use the `language` query parameter to pin one or constrain detection to a list. See [Languages](/speech-to-text/features/languages/). To decide which language to pin on future connections, set `include_language=true` to receive the detected language on each `turn_end_candidate`. ## How It Works [Section titled “How It Works”](#how-it-works) 1. **Connect** - Open a WebSocket connection to `wss://api.reson8.dev/v1/speech-to-text/turns` with an [authentication](/authentication/) header. 2. **Configure** - Use [query parameters](/api/speech-to-text/turns/#query-parameters) to set the audio encoding, language, phrases, and turn probability thresholds. 3. **Stream audio** - Send audio data as binary WebSocket frames. 4. **Receive turn events** - The server emits JSON text frames for turn start, candidate transcripts, and the final turn end. 5. **Optionally flush** - Send `{"type":"flush_request"}` as a JSON text frame to end the current turn immediately. 6. **Close** - Close the WebSocket connection when done. See the [API reference](/api/speech-to-text/turns/) for full details on messages, fields, and error codes. ## Sequence Diagram [Section titled “Sequence Diagram”](#sequence-diagram) ``` sequenceDiagram Client->>Server: Create Connection (auth + query params) activate Server Client->>Server: Audio (binary) Server->>Client: Turn Start Client->>Server: Audio (binary) Server->>Client: Turn End Candidate (text) Client->>Server: Audio (binary) Server->>Client: Turn End Candidate (text) Client->>Server: Audio (binary) Server->>Client: Turn End Client->>Server: Audio (binary) Server->>Client: Turn Start Client->>Server: Close Connection deactivate Server ``` ## Ping / Pong [Section titled “Ping / Pong”](#ping--pong) The server sends a WebSocket ping frame every 30 seconds to keep the connection alive. This uses the built-in WebSocket ping/pong mechanism - clients should respond with a pong for every ping received. Most WebSocket libraries and browsers handle this automatically. ## Flush a Turn [Section titled “Flush a Turn”](#flush-a-turn) Send a `flush_request` text message when the client needs to finalize the current turn without waiting for turn-end detection: ```json { "type": "flush_request" } ``` For an active turn, the server responds with a final `turn_end_candidate` and then `turn_end`. See the [API reference](/api/speech-to-text/turns/#sending-messages) for the complete message format. # Support > Reson8 support: check real-time platform availability and incident updates on the service status page. ## Service Status [Section titled “Service Status”](#service-status) Check [status.reson8.dev](https://status.reson8.dev) for real-time platform availability and incident updates. If you’re running into unexpected errors or timeouts, it may be a known issue we’re already working on. The status page allows you to subscribe to updates. ## Contact Us [Section titled “Contact Us”](#contact-us) For questions or feedback, reach out at . To help us get back to you quickly, please include: * Which service or feature you’re using * The organization name and/or client ID involved * Any error messages or HTTP status codes you’re seeing * A brief description of what you were trying to do and what happened instead # Versioning > How the Reson8 API evolves: backwards compatibility guarantees, additive changes, and what a new major version would mean. The API is versioned in the URL path; the current version is `v1` (e.g. `/v1/speech-to-text/prerecorded`). ## Backwards Compatibility [Section titled “Backwards Compatibility”](#backwards-compatibility) Within a version, changes are backwards compatible. We may add new response properties and new optional parameters, but we do not remove or repurpose existing ones unless we have verified that a feature is unused. Write clients that ignore unknown fields, so additive changes never break them. ## Staying Up to Date [Section titled “Staying Up to Date”](#staying-up-to-date) Improvements to the transcription pipeline are rolled out server-side. Requests always run on the current pipeline - there is nothing to upgrade or migrate on your side. A new major version (e.g. `v2`) would only be introduced for changes that cannot be made compatibly. When that happens we will announce it, and the previous version will keep being supported - existing integrations continue to work without changes. Until then, there is no versioning to be concerned with.