> For the full page index, fetch https://echonova.ai/llms.txt

# Stream speech to text

Send live PCM16 audio and receive a final transcript for each segment.

## Endpoint

`WS wss://api.echonova.ai/v1/speech-to-text/stream`

Open one socket per live audio source. Send mono PCM16 little-endian audio as binary frames. Send end_segment when the speaker pauses to get that segment's transcript right away; without it the server cuts a segment every 15 seconds. Each transcript is final: there are no interim or partial results. Each segment is charged at the Live Speech to Text rate when its transcript is sent.

> [!NOTE]
> Connect from your server. A browser cannot set the `Authorization` header on a WebSocket, so a web page cannot call this endpoint.

## Authentication

Send `Authorization: Bearer YOUR_API_KEY` on the upgrade request. Keys are created in the portal under Developers. See [Get an API key](/docs/developers/get-an-api-key).

## Query parameters

| Field       | Type    | Required | Limits                                | Description                                                                                           |
| ----------- | ------- | -------- | ------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| sample_rate | integer | Yes      | 8000 to 48000                         | Sample rate of the audio you send, in Hz.                                                             |
| language    | string  | Yes      | one of ar, en                         | Spoken language. Required. ar for Arabic and mixed Arabic/English; en only for wholly English speech. |
| encoding    | string  | No       | always pcm_s16le, default "pcm_s16le" | Audio encoding. Only 16-bit signed little-endian mono PCM is accepted.                                |

## Messages you send

### `audio`

Binary frame of mono PCM16 little-endian audio at the sample_rate you connected with. At most 64 KiB.

### `end_segment`

Transcribe the audio sent since the last segment now. Send it when the speaker pauses.

| Field | Type   | Required | Limits             | Description |
| ----- | ------ | -------- | ------------------ | ----------- |
| type  | string | Yes      | always end_segment |             |

```json
{ "type": "end_segment" }
```

### `stop`

Transcribe any remaining audio, send the last transcripts and session.ended, then close.

| Field | Type   | Required | Limits      | Description |
| ----- | ------ | -------- | ----------- | ----------- |
| type  | string | Yes      | always stop |             |

```json
{ "type": "stop" }
```

### `ping`

Optional keep-alive. The server answers with pong.

| Field | Type   | Required | Limits      | Description |
| ----- | ------ | -------- | ----------- | ----------- |
| type  | string | Yes      | always ping |             |

```json
{ "type": "ping" }
```

## Messages you receive

### `session.started`

Sent once after the socket opens. Start sending audio after it arrives.

| Field             | Type    | Required | Limits                 | Description                                  |
| ----------------- | ------- | -------- | ---------------------- | -------------------------------------------- |
| type              | string  | Yes      | always session.started |                                              |
| sessionId         | string  | Yes      |                        | Identifies this session in support requests. |
| language          | string  | Yes      | one of ar, en          | The language you connected with.             |
| sampleRate        | integer | Yes      |                        | The sample rate the server expects, in Hz.   |
| maxSessionSeconds | integer | Yes      |                        | Seconds until the server ends the session.   |

```json
{ "type": "session.started", "sessionId": "c8a1f2e4-5b6d-4e7f-9a0b-1c2d3e4f5a6b", "language": "ar", "sampleRate": 16000, "maxSessionSeconds": 1500 }
```

### `transcript`

The final transcript of one segment. Segments can arrive slightly after you send end_segment; match them by segmentId.

| Field           | Type    | Required | Limits            | Description                                            |
| --------------- | ------- | -------- | ----------------- | ------------------------------------------------------ |
| type            | string  | Yes      | always transcript |                                                        |
| segmentId       | string  | Yes      |                   | Identifies the segment.                                |
| text            | string  | Yes      |                   | The transcript. Empty when the segment held no speech. |
| language        | string  | No       |                   | The detected language, ar or en.                       |
| durationSeconds | number  | Yes      |                   | Length of the segment's audio, in seconds.             |
| creditsConsumed | number  | Yes      |                   | Credits charged for this segment.                      |
| isFinal         | boolean | Yes      | always true       | Always true: there are no interim results.             |

```json
{ "type": "transcript", "segmentId": "6720f3a1c4b5d6e7f8a9b0c1", "text": "مرحبا، كيف يمكنني مساعدتك اليوم؟", "language": "ar", "durationSeconds": 2.4, "creditsConsumed": 8, "isFinal": true }
```

### `session.ended`

The last message. The server closes the socket right after it.

| Field           | Type   | Required | Limits                                                                                           | Description                                   |
| --------------- | ------ | -------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------- |
| type            | string | Yes      | always session.ended                                                                             |                                               |
| reason          | string | Yes      | one of stopped, max_duration, idle_timeout, insufficient_balance, spending_limit, upstream_error | Why the session ended.                        |
| totalSeconds    | number | Yes      |                                                                                                  | Seconds of audio transcribed in this session. |
| creditsConsumed | number | Yes      |                                                                                                  | Credits charged in this session.              |

```json
{ "type": "session.ended", "reason": "stopped", "totalSeconds": 42.6, "creditsConsumed": 142 }
```

### `error`

Something went wrong. Fatal errors are followed by a close frame; others leave the socket open.

| Field     | Type   | Required | Limits                                                                                                                            | Description                                     |
| --------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
| type      | string | Yes      | always error                                                                                                                      |                                                 |
| requestId | string | No       |                                                                                                                                   | Text to Speech only: the synthesis that failed. |
| code      | string | Yes      | one of invalid_message, frame_too_large, validation_failed, voice_not_found, insufficient_balance, spending_limit, upstream_error | Machine-readable reason.                        |
| message   | string | Yes      |                                                                                                                                   | Human-readable explanation.                     |

```json
{ "type": "error", "code": "validation_failed", "message": "text must be shorter than or equal to 500 characters" }
```

### `pong`

The answer to ping.

| Field | Type   | Required | Limits      | Description |
| ----- | ------ | -------- | ----------- | ----------- |
| type  | string | Yes      | always pong |             |

```json
{ "type": "pong" }
```

## Close codes

| Code | Name                 | When                                                                                                                                        |
| ---- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| 1000 | Normal               | You sent stop, or you closed the socket.                                                                                                    |
| 1001 | Going away           | The server is restarting; reconnect.                                                                                                        |
| 1008 | Policy violation     | A frame was not valid JSON, was an unknown type, or was larger than 64 KiB.                                                                 |
| 1009 | Message too big      | A frame was larger than 1 MiB; the socket closes without an error message.                                                                  |
| 1011 | Server error         | Speech to Text: the speech service failed; reconnect and retry. On Text to Speech a failed synthesis sends error and the socket stays open. |
| 4002 | Insufficient balance | The workspace ran out of credits mid-session.                                                                                               |
| 4008 | Timeout              | No frame for 120 seconds, or the session reached its maximum length.                                                                        |
| 4029 | Spending limit       | The key reached its Credit Limit mid-session.                                                                                               |

## Limits

| Limit              | Value                                                                    |
| ------------------ | ------------------------------------------------------------------------ |
| Frame size         | 64 KiB per binary frame; about 100 ms of audio per frame is recommended. |
| Segment length     | 15 seconds; longer speech is cut and transcribed in parts.               |
| Session length     | 25 minutes; the server then ends the session with reason max_duration.   |
| Idle timeout       | 120 seconds without any frame.                                           |
| Concurrent streams | 10 open streams per workspace, STT and TTS together.                     |
| Connection rate    | 100 connection attempts per minute per key.                              |

## Handshake errors

Returned as plain HTTP, with the usual error envelope, instead of the upgrade. Nothing is charged.

| Status | errorMessage                                                 | When                                                                                                                                              | Fix                                                                           |
| ------ | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| 400    | sample_rate must be an integer from 8000 to 48000            | sample_rate is missing, not a whole number, or out of range.                                                                                      | Connect with ?sample_rate= set to your audio's rate.                          |
| 400    | language must be one of ar, en                               | language is missing or not ar or en.                                                                                                              | Connect with ?language= set to ar or en.                                      |
| 400    | encoding must be pcm_s16le                                   | encoding is not an allowed value.                                                                                                                 | Leave encoding out, or send pcm_s16le.                                        |
| 401    | Unauthorized                                                 | The Authorization header is missing or is not Bearer ek\_..., no key matches the token, or the key is deleted, disabled or its account is closed. | Send Authorization: Bearer ek\_... with an enabled key from this environment. |
| 402    | Insufficient subscription balance                            | The workspace balance is empty.                                                                                                                   | Add credits to the workspace.                                                 |
| 403    | This API key does not have access to Speech-to-Text services | The key is restricted and Speech to Text is not among its services.                                                                               | Edit the key and grant Speech to Text, or use another key.                    |
| 403    | Subscription expired or inactive                             | The subscription is in a state other than active or trial.                                                                                        | Renew the subscription.                                                       |
| 403    | Subscription not found                                       | The workspace that owns the key has no subscription.                                                                                              | Check the workspace's plan in the portal.                                     |
| 429    | ThrottlerException: Too Many Requests                        | The key opened more than 100 connections in 60 seconds.                                                                                           | Wait Retry-After seconds, then reconnect.                                     |
| 429    | Too many concurrent streams                                  | The workspace already has 10 streams open.                                                                                                        | Close a stream, or reuse one socket for many syntheses.                       |
| 429    | Monthly spending limit exhausted                             | The key has reached its Credit Limit for this billing cycle.                                                                                      | Raise or clear the limit on the key, or wait for the next cycle.              |
| 503    | Streaming is not enabled                                     | Streaming is turned off on this environment.                                                                                                      | Contact support.                                                              |
