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

# Stream text to speech

Send text and receive PCM16 audio as it is generated.

## Endpoint

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

Keep one socket open and send a synthesize message for each sentence or reply. The server answers with start, then binary PCM16 frames, then done. Audio is generated a few seconds of speech at a time, so the first audio arrives well before the whole text is spoken. Only one synthesis runs at a time: send cancel, or a new synthesize, to stop the current one (barge-in). Each synthesize is charged per character when it starts; a cancelled synthesis is still charged.

> [!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).

## Messages you send

### `synthesize`

Speak this text. Cancels any synthesis still running on this socket.

| Field     | Type    | Required | Limits                                 | Description                                                                                                     |
| --------- | ------- | -------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| type      | string  | Yes      | always synthesize                      |                                                                                                                 |
| requestId | string  | No       | max 64 chars                           | Your id for this synthesis, echoed in start, done, cancelled and error. Generated when you leave it out.        |
| text      | string  | Yes      | 1 to 500 chars                         | The text to speak, 1 to 500 characters.                                                                         |
| voiceId   | string  | Yes      |                                        | The id of a voice from List available voices. The voice fixes the dialect.                                      |
| speed     | number  | No       | 0.5 to 2, default 1                    | How fast the voice speaks.                                                                                      |
| cfg       | number  | No       | 1 to 5, default 2                      | How closely the voice follows the text. Leave it at the default unless you are tuning.                          |
| numSteps  | integer | No       | one of 16, 32, 48, 64, 128, default 48 | Diffusion steps. More steps are slower and cleaner.                                                             |
| tashkeel  | boolean | No       | default false                          | Adds the vowel marks to Arabic text before it is read. Applies to Fusha voices only.                            |
| normalize | boolean | No       | default true                           | Reads numbers, dates, times, money, units, URLs and e-mails the way a speaker of the voice's dialect says them. |

```json
{ "type": "synthesize", "requestId": "reply-1", "text": "أهلاً بك، كيف يمكنني مساعدتك اليوم؟", "voiceId": "66e5ef3a9c1d2b4f8a7e6c01" }
```

### `cancel`

Stop the running synthesis. The server answers with cancelled and sends no more audio for it.

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

```json
{ "type": "cancel" }
```

### `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

### `start`

Sent before the first audio frame of a synthesis. Use its sampleRate to play the audio.

| Field      | Type    | Required | Limits           | Description                             |
| ---------- | ------- | -------- | ---------------- | --------------------------------------- |
| type       | string  | Yes      | always start     |                                         |
| requestId  | string  | Yes      |                  |                                         |
| sampleRate | integer | Yes      |                  | Sample rate of the audio frames, in Hz. |
| encoding   | string  | Yes      | always pcm_s16le |                                         |
| channels   | integer | Yes      | always 1         |                                         |

```json
{ "type": "start", "requestId": "reply-1", "sampleRate": 24000, "encoding": "pcm_s16le", "channels": 1 }
```

### `audio`

Binary frame of mono PCM16 little-endian audio at the sampleRate from start. Play frames in the order they arrive.

### `done`

All audio for the synthesis has been sent.

| Field           | Type    | Required | Limits      | Description                           |
| --------------- | ------- | -------- | ----------- | ------------------------------------- |
| type            | string  | Yes      | always done |                                       |
| requestId       | string  | Yes      |             |                                       |
| durationSeconds | number  | Yes      |             | Length of the audio sent, in seconds. |
| characters      | integer | Yes      |             | Characters charged.                   |
| creditsConsumed | number  | Yes      |             | Credits charged for this synthesis.   |

```json
{ "type": "done", "requestId": "reply-1", "durationSeconds": 2.9, "characters": 36, "creditsConsumed": 36 }
```

### `cancelled`

The synthesis stopped. No more audio follows for this requestId.

| Field     | Type   | Required | Limits           | Description |
| --------- | ------ | -------- | ---------------- | ----------- |
| type      | string | Yes      | always cancelled |             |
| requestId | string | Yes      |                  |             |

```json
{ "type": "cancelled", "requestId": "reply-1" }
```

### `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                                                   |
| ------------------- | ------------------------------------------------------- |
| Text length         | 500 characters per synthesize message.                  |
| Syntheses at a time | 1 per socket; a new synthesize cancels the current one. |
| Session length      | 30 minutes.                                             |
| 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                                                                           |
| ------ | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| 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 Text-to-Speech services | The key is restricted and Text to Speech is not among its services.                                                                               | Edit the key and grant Text to Speech, 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.                                                              |
