> ## Documentation Index
> Fetch the complete documentation index at: https://docs.finesse.flamapp.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Stream speech (SSE)

> Stream synthesized audio as Server-Sent Events for low-latency playback.

Streams synthesis as **Server-Sent Events** so you can start playing audio before the full clip is
ready. The request body is identical to [`/speak`](/voice-api/synthesize-speech); the response is an
`text/event-stream` body instead of a single file.

#### Authorization

<ParamField header="X-API-Key" type="string" required>
  Your workspace API key (`flv_…`).
</ParamField>

#### Path parameters

<ParamField path="id" type="string" required>
  The voice id.
</ParamField>

#### Body

<ParamField body="text" type="string" required initialValue="Streaming hello from Finesse.">
  The text to synthesize. Maximum **10,000 characters**.
</ParamField>

<ParamField body="language" type="string">
  BCP-47 language code (for example `en`, `hi`). Defaults to the voice's primary language.
</ParamField>

<ParamField body="output_format" type="string">
  Chunk encoding. One of `s16le`, `s24le`, `s32le`, `f32le`, `f64le` (raw, headerless PCM). Chunks
  arrive as base64-encoded PCM either way; the format decides the sample width.
</ParamField>

<ParamField body="sample_rate" type="integer" default={24000} initialValue={24000}>
  Output sample rate in Hz. Defaults to the provider's native rate (`24000`); values above it are
  upsampled. The effective rate is reported in the `start` event.
</ParamField>

<ParamField body="num_step" type="integer" default={32} initialValue={32}>
  Number of inference steps, `1`–`32` (a quality/latency trade-off). Provider default (`32`) if
  omitted.
</ParamField>

<ParamField body="expressiveness" type="number" initialValue={0.5}>
  Style/expressiveness control, `0.0`–`1.0`. Provider default if omitted.
</ParamField>

#### Response headers

<ResponseField name="Content-Type" type="string">
  `text/event-stream` (with `Cache-Control: no-cache` and `X-Accel-Buffering: no` so proxies don't
  buffer the stream).
</ResponseField>

<ResponseField name="X-Billing-Remaining" type="string">
  Your remaining character allowance for the month (present when billing is active). Streaming is
  metered per character of `text`, same as [`/speak`](/voice-api/synthesize-speech).
</ResponseField>

## Event format

Each event is delimited per the SSE spec (`event:` line, `data:` JSON line, blank line):

```text theme={null}
event: start
data: {"type":"start","sample_rate":24000,"audio_format":"s16le"}

event: chunk
data: {"type":"chunk","index":0,"audio_b64":"<base64 PCM>"}

event: chunk
data: {"type":"chunk","index":1,"audio_b64":"<base64 PCM>"}

event: done
data: {"type":"done","audio_duration_s":3.42,"generation_id":"665f1a2b3c4d5e6f7a8b9f10","audio_url":"https://storage.googleapis.com/.../speech.wav"}
```

<ResponseField name="start" type="event">
  Sent once at the beginning. `sample_rate` is the **effective** output rate and `audio_format` the
  PCM encoding of the chunks - use these (not what you requested) to configure playback.
</ResponseField>

<ResponseField name="chunk" type="event">
  One per audio chunk, in order. `index` is the 0-based position; `audio_b64` is base64-encoded raw
  PCM audio to append to your buffer.
</ResponseField>

<ResponseField name="done" type="event">
  Sent once when synthesis is complete; no more chunks follow. Carries `audio_duration_s` (total
  seconds of audio), and - when object storage is enabled - `generation_id` and a public `audio_url`
  of the full clip as a WAV file, so you can link or re-download it without reassembling the chunks.
</ResponseField>

<ResponseField name="error" type="event">
  Sent if synthesis fails. Includes a provider `code` and `message`. The stream ends after it.
</ResponseField>

<Note>
  Streaming requires the `streaming` feature on your plan; without it the request returns `403` when
  billing enforcement is active, and an exhausted character allowance returns `402` - see
  [Errors](/voice-api/errors). Streamed synthesis is never served from cache.
</Note>

<CodeGroup>
  ```bash cURL theme={null}
  curl -N -X POST \
    "https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices/665f1a2b3c4d5e6f7a8b9c0d/stream" \
    -H "X-API-Key: $FLAM_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"text":"Streaming hello from Finesse.","language":"en"}'
  ```

  ```javascript JavaScript theme={null}
  const id = "665f1a2b3c4d5e6f7a8b9c0d";
  const res = await fetch(
    `https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices/${id}/stream`,
    {
      method: "POST",
      headers: {
        "X-API-Key": process.env.FLAM_API_KEY,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ text: "Streaming hello from Finesse.", language: "en" }),
    },
  );

  const reader = res.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";
  for (;;) {
    const { done, value } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });
    let sep;
    while ((sep = buffer.indexOf("\n\n")) !== -1) {
      const frame = buffer.slice(0, sep);
      buffer = buffer.slice(sep + 2);
      const line = frame.split("\n").find((l) => l.startsWith("data:"));
      if (!line) continue;
      const ev = JSON.parse(line.slice(5).trim());
      if (ev.type === "chunk") {
        const pcm = Buffer.from(ev.audio_b64, "base64"); // append to your audio buffer
      } else if (ev.type === "done") {
        break;
      } else if (ev.type === "error") {
        throw new Error(ev.message ?? ev.code);
      }
    }
  }
  ```

  ```python Python theme={null}
  import os, base64, json, requests

  voice_id = "665f1a2b3c4d5e6f7a8b9c0d"
  with requests.post(
      f"https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices/{voice_id}/stream",
      headers={"X-API-Key": os.environ["FLAM_API_KEY"], "Content-Type": "application/json"},
      json={"text": "Streaming hello from Finesse.", "language": "en"},
      stream=True,
  ) as res:
      res.raise_for_status()
      for raw in res.iter_lines():
          if not raw or not raw.startswith(b"data:"):
              continue
          ev = json.loads(raw[5:].strip())
          if ev["type"] == "chunk":
              pcm = base64.b64decode(ev["audio_b64"])  # append to your audio buffer
          elif ev["type"] == "done":
              break
          elif ev["type"] == "error":
              raise RuntimeError(ev.get("message") or ev.get("code"))
  ```
</CodeGroup>

## Prompt

Copy this into your coding agent (Claude Code, Cursor, Codex…) to implement this endpoint.

```text theme={null}
You are implementing a typed client for one Finesse Voice API endpoint: "Stream speech (SSE)".
Everything you need is in this prompt. Do not invent anything that is not specified here.

WHAT TO BUILD
- A function streamSpeech(voiceId, params, options) (name it per the project's conventions) that
  streams synthesis as Server-Sent Events and exposes typed events in order, as an async iterator
  or callbacks, whichever fits the stack. Support cancellation (AbortSignal or equivalent).
- A convenience helper that consumes the stream and returns { pcm: bytes, sampleRate, audioFormat,
  audioDurationS, audioUrl?, generationId? }.
- Use a streaming HTTP read (fetch + res.body reader in TypeScript, requests stream=True / httpx
  stream in Python). Do not add an EventSource dependency: EventSource cannot send POST bodies or
  custom headers.
- Detect the stack first: look for package.json, tsconfig.json, pyproject.toml,
  requirements.txt, go.mod, Gemfile, composer.json and similar. Use the project's language, HTTP
  client, module layout, error-handling conventions and test runner. If there is no project yet or
  the stack is unclear, use TypeScript on Node 18+ with the built-in fetch and no new dependencies.
- If the project already has a Finesse API client module, add this to it (reuse its auth, base
  URL, error type and retry helper) instead of creating a parallel one.
- Keep the base URL (https://fi.production.flamapis.com) in one constant or config value.

ENDPOINT
- Method: POST
- URL: https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices/{id}/stream
- Request Content-Type: application/json
- Success response Content-Type: text/event-stream (with Cache-Control: no-cache and
  X-Accel-Buffering: no so proxies do not buffer)
- The voice must be "ready" and belong to the key's workspace.

PARAMETERS
- id (path, string, required): the voice id, a 24-character hex string (for example
  "665f1a2b3c4d5e6f7a8b9c0d"). Validate it locally against /^[0-9a-f]{24}$/i and URL-encode it.

REQUEST BODY (same fields as the one-shot /speak endpoint, except output_format)
- text (string, required): maximum 10,000 characters (check locally, counting Unicode code points;
  reject empty text).
- language (string, optional): BCP-47 code (e.g. "en", "hi"). Defaults to the voice's
  primary_language.
- output_format (string, optional): chunk encoding, one of "s16le", "s24le", "s32le", "f32le",
  "f64le" (raw, headerless PCM). Chunks arrive base64-encoded either way; the format decides the
  sample width. Do not offer "wav" for streaming.
- sample_rate (integer, optional, default 24000): output rate in Hz; values above the native 24000
  are upsampled. The EFFECTIVE rate is reported in the start event.
- num_step (integer, optional, default 32): 1-32, quality vs latency.
- expressiveness (number, optional): 0.0-1.0. Provider default when omitted.
- Omit optional fields the caller did not set.

    { "text": "Streaming hello from Finesse.", "language": "en" }

AUTHENTICATION
- Send the header  X-API-Key: <workspace API key>  on every request. Keys start with "flv_".
- Read the key from the environment variable FLAM_API_KEY (or the project's existing secrets or
  config mechanism). Fail fast with a clear error if it is missing, before making any request.
- Never hard-code, log or commit the key, and never ship it in browser/client-side code: call this
  from server-side code only. The key can synthesize with every voice in its workspace.
- A key is bound to one workspace. Referencing a voice or job from another workspace returns 404.

EVENT STREAM FORMAT
Each event follows the SSE spec: an "event:" line, a "data:" line with JSON, then a blank line.

    event: start
    data: {"type":"start","sample_rate":24000,"audio_format":"s16le"}

    event: chunk
    data: {"type":"chunk","index":0,"audio_b64":"<base64 PCM>"}

    event: chunk
    data: {"type":"chunk","index":1,"audio_b64":"<base64 PCM>"}

    event: done
    data: {"type":"done","audio_duration_s":3.42,"generation_id":"665f1a2b3c4d5e6f7a8b9f10","audio_url":"https://storage.googleapis.com/.../speech.wav"}

Event types (dispatch on the JSON "type" field):
- start: sent once first. sample_rate is the EFFECTIVE output rate and audio_format the PCM
  encoding of the chunks. Use these, not what you requested, to configure playback.
- chunk: one per audio chunk, in order. index is the 0-based position; audio_b64 is base64 raw PCM
  to append to your buffer. Append in arrival order and do not fail if index is missing.
- done: sent once when synthesis is complete; no more chunks follow. Carries audio_duration_s
  (total seconds of audio) and, when object storage is enabled, generation_id and a public
  audio_url of the full clip as a WAV file (optional fields).
- error: sent if synthesis fails, with a provider "code" and "message". The stream ends after it;
  surface it as a typed error.

Parser requirements:
- Buffer across network reads; split events on blank lines; accept both \n and \r\n line endings;
  join multiple "data:" lines of one event with \n; ignore comment lines starting with ":" and
  unknown event types.
- If the stream closes without a done or error event, treat it as an incomplete-stream error, not
  success.

ERRORS BEFORE THE STREAM STARTS
A non-200 status means no stream: read the body as JSON (never as SSE). Errors use the standard
envelope:

    {
      "status": 400,
      "error": true,
      "message": "text too long (max 10000 chars)"
    }

Billing and plan denials do NOT use the envelope. They return a flat JSON body whose "error"
field is a string, not a boolean:

    { "error": "plan limit reached", "feature": "max_voices", "limit": 10 }
    { "error": "quota_exhausted", "unit": "character", "limit": 100000, "used": 100000 }

The "error" string is the denial reason (for example quota_exhausted, insufficient_credits,
limit_reached, feature_disabled, or one of the messages above). Tell the two shapes apart by
checking whether "error" is a boolean (envelope) or a string (flat denial), and expose
feature / unit / limit / used on your error type when present.

- 400: invalid request, text over 10,000 characters, or the voice is not ready yet. Do not retry.
- 401: X-API-Key missing, malformed, invalid or revoked. Do not retry.
- 402: character allowance exhausted (flat body). Do not retry.
- 403: the plan does not include the "streaming" feature (when billing enforcement is active), or
  another plan/permission denial (flat body). Do not retry.
- 404: voice not found, including voices owned by another workspace. Do not retry.
- 422: the provider rejected the input. Do not retry.
- 429: rate limited. 502: provider warming up or unavailable. Retry these with backoff.
- Surface failures as one typed error (for example FinesseApiError) carrying: HTTP status, message
  (the envelope "message" or the flat "error" string), optional feature / unit / limit / used, a
  retryable flag, and the raw body for debugging.
- Parse error bodies defensively: if a body is not valid JSON, keep the raw text in the error.

RETRIES
- Retry 429, 502 and network errors ONLY if no event has been received yet, with exponential
  backoff plus jitter (at most 3 retries). The first request after a long idle can be slow while
  the engine warms up; use a generous, configurable time-to-first-event timeout.
- Never auto-retry after the stream has started or after an error event: streamed synthesis is
  never served from cache, so every attempt is billed again. Surface the error and let the caller
  decide.
- Never retry 400, 401, 402, 403, 404 or 422.

BILLING
- Metered per character of text, same as /speak, against the monthly allowance.
- Requires the "streaming" feature on the plan.
- Capture X-Billing-Remaining (remaining characters, present when billing is active) as an
  optional number on the result.

ACCEPTANCE CRITERIA
- streamSpeech yields typed start / chunk / done / error events in order and decodes chunk audio
  from base64 to bytes.
- The helper returns the concatenated PCM with the effective sample_rate and audio_format from the
  start event, plus audio_duration_s, audio_url and generation_id from done when present.
- Pre-stream failures are parsed as JSON into the typed error; error events and incomplete streams
  reject; cancellation aborts the HTTP request.
- The key is never logged or sent from client-side code.

TEST PLAN
- Unit tests feeding a fake SSE body split at awkward byte boundaries (mid-line, mid-JSON, CRLF):
  start + 3 chunks + done; an error event; a stream that ends without done; unknown event ignored;
  pre-stream 403 flat body and 400 envelope; retry on 502 before the first event; no retry after a
  chunk was received.
- Smoke script:
- Get a ready voice id from FLAM_VOICE_ID, or by calling GET https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices (same
  X-API-Key header; voices are in data.voices) and picking the first one whose status is "ready".
  - Stream "Streaming hello from Finesse.", print the start event, the number of chunks, total PCM
    bytes, audio_duration_s and audio_url, and the time to the first chunk. Exit non-zero on error.

RULES
- Implement only what this spec describes. Do not invent parameters, fields, headers, endpoints or
  error codes. If something is ambiguous, missing, or conflicts with the existing codebase, stop
  and ask me before guessing.
- Match the project's existing style and conventions; do not refactor unrelated code.
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.