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

# Synthesize speech

> Turn text into a single audio file using a cloned voice.

Synthesizes `text` with the given voice and returns the **raw audio bytes** in the response body. The
voice must be enrolled (status `ready`).

<Note>
  Unlike the JSON endpoints, a successful response is the audio file itself - not a JSON envelope. Errors
  still return the [standard envelope](/voice-api/errors).
</Note>

#### 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="Hello from Finesse.">
  The text to synthesize. Maximum **10,000 characters**. Billed per character against your plan's
  monthly allowance; repeating an identical request (same voice, text, and parameters) is served
  from cache and not billed again.
</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" default="wav">
  Output encoding. One of `wav`, `s16le`, `s24le`, `s32le`, `f32le`, `f64le`. `wav` is a self-contained
  file; the `s*`/`f*` formats are raw, headerless PCM.
</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.
</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">
  The audio MIME type, e.g. `audio/wav` for `output_format: wav`.
</ResponseField>

<ResponseField name="X-Sample-Rate" type="string">
  The sample rate of the returned audio, in Hz (when reported by the provider).
</ResponseField>

<ResponseField name="X-Audio-URL" type="string">
  A public URL to the stored audio file, when object storage is enabled. Useful for download/share.
</ResponseField>

<ResponseField name="X-Billing-Remaining" type="string">
  Your remaining character allowance for the month (present when billing is active). An exhausted
  allowance returns `402` when enforcement is on - see [Errors](/voice-api/errors).
</ResponseField>

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

  ```javascript JavaScript theme={null}
  import { writeFile } from "node:fs/promises";

  const id = "665f1a2b3c4d5e6f7a8b9c0d";
  const res = await fetch(
    `https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices/${id}/speak`,
    {
      method: "POST",
      headers: {
        "X-API-Key": process.env.FLAM_API_KEY,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ text: "Hello from Finesse.", language: "en", output_format: "wav" }),
    },
  );
  if (!res.ok) throw new Error(`Synthesis failed: ${res.status}`);
  await writeFile("speech.wav", Buffer.from(await res.arrayBuffer()));
  ```

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

  voice_id = "665f1a2b3c4d5e6f7a8b9c0d"
  res = requests.post(
      f"https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices/{voice_id}/speak",
      headers={"X-API-Key": os.environ["FLAM_API_KEY"], "Content-Type": "application/json"},
      json={"text": "Hello from Finesse.", "language": "en", "output_format": "wav"},
  )
  res.raise_for_status()
  with open("speech.wav", "wb") as f:
      f.write(res.content)
  ```
</CodeGroup>

See [Synthesize parameters](/voice-api/synthesize-parameters) for the full field reference.

## 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: "Synthesize speech".
Everything you need is in this prompt. Do not invent anything that is not specified here.

WHAT TO BUILD
- A function synthesizeSpeech(voiceId, params) (name it per the project's conventions) that turns
  text into ONE audio file with the given voice and returns:
    { audio: bytes, contentType: string, sampleRate?: number, audioUrl?: string,
      billingRemaining?: number }
  Use the stack's natural byte type (Buffer/Uint8Array, bytes, []byte).
- Optionally a thin wrapper that writes the audio to a file path, if the project needs it.
- 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}/speak
- Request Content-Type: application/json
- The voice must be enrolled (status "ready"), and must 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
- text (string, required): the text to synthesize. Maximum 10,000 characters; over the limit
  returns 400 "text too long (max 10000 chars)". Check locally: reject empty text and count
  characters (Unicode code points, not bytes), e.g. [...text].length in JavaScript.
- language (string, optional): BCP-47 code (e.g. "en", "hi", "es"). Defaults to the voice's
  primary_language.
- output_format (string, optional, default "wav"): one of
    wav    - self-contained WAV file (recommended for playback/download)
    s16le  - signed 16-bit little-endian PCM (headerless)
    s24le  - signed 24-bit little-endian PCM (headerless)
    s32le  - signed 32-bit little-endian PCM (headerless)
    f32le  - 32-bit float little-endian PCM (headerless)
    f64le  - 64-bit float little-endian PCM (headerless)
  The headerless PCM formats need a known sample rate and channel count to play.
- sample_rate (integer, optional, default 24000): output rate in Hz. Defaults to the provider's
  native rate (24000); values above it are upsampled.
- num_step (integer, optional, default 32): inference steps, 1-32; a quality vs latency trade-off.
- expressiveness (number, optional): style/expressiveness control, 0.0-1.0. Provider default when
  omitted.
- Validate the enums and ranges locally and omit optional fields the caller did not set.

    {
      "text": "The quick brown fox jumps over the lazy dog.",
      "language": "en",
      "output_format": "wav",
      "sample_rate": 24000,
      "expressiveness": 0.5
    }

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.

SUCCESS RESPONSE (200): RAW AUDIO BYTES, NOT JSON
- The body is the audio file itself. Do not parse it as JSON; read it as binary
  (e.g. await res.arrayBuffer()).
- Check the status BEFORE reading the body: only a 2xx body is audio.
- Response headers to capture:
    Content-Type         the audio MIME type, e.g. audio/wav for output_format "wav"
    X-Sample-Rate        sample rate of the returned audio in Hz (when reported); parse to a number
    X-Audio-URL          public URL of the stored audio file, when object storage is enabled
    X-Billing-Remaining  remaining monthly character allowance, when billing is active
  All of these except Content-Type may be absent: type them as optional.

ERROR RESPONSES (JSON, even though success is binary)
Errors use the standard envelope:

    {
      "status": 400,
      "error": true,
      "message": "Voice is not enrolled yet"
    }

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: missing or invalid field, text over 10,000 characters, or the voice is not
  ready yet ("Voice is not enrolled yet"). Do not retry.
- 401: X-API-Key missing, malformed, invalid or revoked. Do not retry.
- 402: monthly character allowance exhausted (flat body, e.g. quota_exhausted with unit, limit,
  used). Do not retry; surface it so the app can prompt a top-up/upgrade.
- 403: not permitted, plan limit reached, or feature disabled for the plan (flat body). Do not
  retry.
- 404: voice not found, including voices owned by another workspace. Do not retry.
- 422: the voice provider rejected the input. Do not retry.
- 429: rate limited. Retry with backoff.
- 502: the voice provider is warming up or temporarily unavailable. Retry 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 AND TIMEOUTS
- Retry 429, 502 and network errors/timeouts with exponential backoff plus jitter (about 1s, 2s,
  4s; at most 3 retries). Retrying is safe here: an identical request (same voice, text and
  parameters) is served from cache and not billed again.
- The first request after a long idle period can be slow while the engine warms up; use a generous
  request timeout (configurable) rather than a short default.
- Never retry 400, 401, 402, 403, 404 or 422.

BILLING
- Billed per character of text against the plan's monthly allowance.
- Repeating an identical request is served from cache and not billed again.
- Expose billingRemaining (from X-Billing-Remaining) so callers can warn before running out; an
  exhausted allowance returns 402 when enforcement is on.

ACCEPTANCE CRITERIA
- synthesizeSpeech validates id, text length and parameter ranges locally, sends only documented
  fields, and returns the audio bytes plus the parsed headers.
- A non-2xx response is never treated as audio; both the envelope and the flat billing body are
  parsed into the typed error.
- 429/502 are retried with backoff; other 4xx are not.
- The key is never logged or sent from client-side code.

TEST PLAN
- Unit tests with mocked HTTP: 200 with audio/wav bytes and all four headers; 200 with only
  Content-Type; 400 "Voice is not enrolled yet"; 402 quota_exhausted flat body; 404; 429 then 200;
  502 then 200; 10,001-character text rejected locally; num_step 0 and 33 rejected locally.
- 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".
  - Synthesize "Hello from Finesse." with output_format "wav", write speech.wav, and assert the
    file starts with the ASCII bytes "RIFF" and Content-Type is audio/wav. Print the byte count,
    X-Sample-Rate, X-Audio-URL and X-Billing-Remaining. 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.