> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://murf.ai/api/docs/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://murf.ai/api/docs/_mcp/server.

> Every error and warning the Falcon 2 streaming and WebSocket APIs can return, with the cause and the fix.

## Overview

The Murf TTS API reports problems in two different ways depending on the transport:

* **HTTP streaming** (`POST /v1/speech/stream`) and **synthesis** (`POST /v1/speech/generate`) return a standard HTTP status code.
* **WebSocket streaming** (`/v1/speech/stream-input`) returns JSON `error` and `warning` frames on the open connection, and uses WebSocket close codes for connection-level failures.

## How to handle errors

Every WebSocket error and warning frame carries a machine-readable code alongside the human-readable text.

* **Branch on `error_code` and `warning_code`, never on the prose.** The wording can change at any time; the codes are stable.
* **`"fatal": true` means nothing further is coming for that context.** Stop waiting for audio or `final` and clean up.
* **A `warning` is not fatal.** Something was ignored or substituted, and synthesis continues.
* **`trace_id` is present on every frame**, including `audio` and `final`, not just on errors. Quote it when you contact support.

## HTTP errors

These apply to `POST /v1/speech/stream` and `POST /v1/speech/generate`.

| Status  | Meaning                                           | How to resolve                                                                                                                                                  |
| ------- | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **400** | Bad Request                                       | The request body is malformed or a field is invalid. Check `voiceId`, `model`, `format` and `sampleRate` against the API reference.                             |
| **402** | Expired subscription or character limit exhausted | Your plan's character quota is used up or the subscription lapsed. Top up or upgrade in the [dashboard](https://murf.ai/api/dashboard).                         |
| **403** | Invalid or expired token/api-key provided         | Send a valid key in the `api-key` header. Note that Murf returns **403, not 401**, for authentication failures.                                                 |
| **408** | Request Timeout                                   | The request took too long to complete or the connection stalled before the body was received. Retry, and raise your client's timeout for long inputs.           |
| **429** | Too Many Requests                                 | You exceeded your plan's concurrency limit. Retry with exponential backoff and cap your in-flight requests. See [Rate Limits](/api/docs/resources/rate-limits). |
| **500** | Internal Server Error                             | Retry with exponential backoff. If it persists, contact support with the request details.                                                                       |
| **503** | Service Unavailable                               | The service is temporarily saturated or the region is unavailable. Retry with backoff, or fall back to `global.api.murf.ai` from a regional host.               |

## WebSocket errors

An error frame looks like this:

```json
{
  "error": "Invalid voice_id: totally-fake. The specified voice is not supported for the selected model (FALCON). Note: Falcon-2 and Gen2 each have their own voice catalog. Use GET /v1/speech/voices?model=FALCON to fetch the list of voices supported by this model, or browse the voice library at: https://murf.ai/api/docs/voices-styles/voice-library.",
  "error_code": "INVALID_VOICE",
  "fatal": true,
  "context_id": "turn_1",
  "trace_id": "0f8bb0ef-8dcb-4815-8b3a-1e2d3c4b5a69"
}
```

| `error_code`                    | Cause                                                                                            | How to resolve                                                                                                                                                                      |
| ------------------------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `INVALID_VOICE`                 | The requested `voiceId` does not exist in the Falcon catalog.                                    | Use `GET /v1/speech/voices?model=FALCON` to list valid voices, or browse the [Voice Library](/api/docs/voices-styles/voice-library). Both `Gordon` and `en-US-gordon` are accepted. |
| `ACTIVE_CONTEXT_LIMIT_EXCEEDED` | You have more simultaneously active `context_id`s than your plan's streaming concurrency allows. | Send `{"end": true}` to close finished contexts, reuse a `context_id` for sequential turns, or raise your limit. See [Rate Limits](/api/docs/resources/rate-limits).                |
| `INVALID_JSON`                  | The frame you sent is not valid JSON.                                                            | Serialize each frame as a single well-formed JSON object. Do not send partial or concatenated payloads.                                                                             |
| `INTERNAL_ERROR`                | Something failed on our side.                                                                    | Retry the context. If it repeats, contact support and quote the `trace_id` from the frame.                                                                                          |

## WebSocket warnings

Warnings mean something in your frame was ignored or substituted. Synthesis continues, so you will still receive audio.

```json
{
  "warning": "min_buffer_size must be an integer between 0 and 1000; keeping the previous value",
  "warning_code": "INVALID_BUFFER_SIZE",
  "context_id": "turn_1"
}
```

| `warning_code`                 | Cause                                                                                    | How to resolve                                                                                                                                                           |
| ------------------------------ | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `INVALID_BUFFER_SIZE`          | `min_buffer_size` was outside 0–1000 or not a number. The previous value is kept.        | Send an integer between 0 and 1000. Numeric strings such as `"100"` are accepted; `"abc"` is not.                                                                        |
| `INVALID_BUFFER_DELAY`         | `max_buffer_delay_in_ms` was outside 0–1000 or not a number. The previous value is kept. | Send an integer between 0 and 1000.                                                                                                                                      |
| `INVALID_PREDICTIVE_CHUNKING`  | `predictive_chunking` was not `strict` or `lenient`.                                     | Send one of the two accepted values, or omit the field to use `strict`.                                                                                                  |
| `INVALID_FIELD_TYPE`           | A field was sent with the wrong JSON type.                                               | Check the field types in the [WebSockets API reference](/api/docs/api-reference/text-to-speech/stream-input).                                                            |
| `NO_VOICE_CONFIG`              | Text arrived in a context that has no `voice_config`, so the default voice was used.     | Send `voice_config` before the first text frame. For a named context, use the same `context_id` on both frames; it does not inherit the default context's configuration. |
| `NO_VOICE_ID`                  | A `voice_config` was sent without a `voiceId`, so the default voice was used.            | Include `voiceId` in `voice_config`. For a named context, also include its `context_id`.                                                                                 |
| `TOO_MANY_GENERATION_REQUESTS` | You are issuing generation requests faster than your plan allows.                        | Slow down, or batch text into fewer frames. Retry after a short backoff. See [Rate Limits](/api/docs/resources/rate-limits).                                             |
| `TEXT_IGNORED_WITH_CLEAR`      | A frame contained both `clear` and `text`. The `text` was dropped.                       | Send `clear` and `text` as two separate frames.                                                                                                                          |

## Connection close codes

| Close code | Cause                                                                                                  | How to resolve                                                                            |
| ---------- | ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- |
| **1008**   | Invalid API key. The HTTP upgrade completes and the socket is then closed with `Invalid api key used.` | Handle the close code, not an HTTP status. Check the `api-key` query parameter or header. |
| **1000**   | Normal close, including the automatic close after 3 minutes of inactivity.                             | Reconnect when you next have text to send.                                                |

> **Note**
>
> An invalid API key on a WebSocket connection is **not** reported as HTTP 401.
> The upgrade succeeds first, so clients that only inspect the HTTP response
> never see the failure. Always handle the close code.

## Recommended handling pattern

```python
import base64, json

async def read_stream(ws):
    audio = bytearray()
    while True:
        msg = json.loads(await ws.recv())

        if msg.get("audio"):
            audio += base64.b64decode(msg["audio"])
        elif msg.get("error"):
            # Branch on the code, never on the prose.
            print("error:", msg["error_code"], msg["error"], msg.get("trace_id"))
            if msg.get("fatal"):
                break
        elif msg.get("warning"):
            print("warning:", msg["warning_code"], msg["warning"])
        elif msg.get("final"):
            break

    return bytes(audio)
```

## FAQs

#### Why do I get 403 instead of 401 for a bad API key?

The HTTP endpoints return **403** for both invalid and expired keys. There
is no 401 response. Over WebSockets the connection is closed with code
**1008** instead, because the HTTP upgrade has already completed by the
time the key is rejected.

#### What causes ACTIVE\_CONTEXT\_LIMIT\_EXCEEDED?

Concurrency is counted by the number of simultaneously active
`context_id`s, not by the number of connections. Closing each turn with
`{"end": true}` frees its slot. See
[Rate Limits](/api/docs/resources/rate-limits) for per-plan numbers.

#### Should I retry on a warning?

No. Warnings are informational and synthesis continues with a default or
the previously accepted value. Fix the offending field in your next frame.

#### What should I include in a support request?

The `trace_id` from any frame in the affected context, the `context_id`,
the endpoint host you connected to, and the approximate time of the
request.