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

# Create a voice

> Create a new (empty) voice in your workspace.

Creates a voice in the workspace that owns the API key. A newly created voice has status `draft` - it
has no audio yet and **cannot synthesize** until you enroll a reference clip with
[`/enroll`](/voice-api/enroll-voice). To create and enroll in a single request, use
[`/clone`](/voice-api/clone-voice) instead.

<Note>
  Creating a voice counts against your plan's `max_voices` limit. Over the cap, the request returns
  `403` with `{ "error": "plan limit reached", "feature": "max_voices", "limit": <n> }`.
</Note>

#### Authorization

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

#### Body

<ParamField body="name" type="string" required>
  Display name for the voice.
</ParamField>

<ParamField body="description" type="string">
  Optional long-form description.
</ParamField>

<ParamField body="clone_method" type="string" default="instant">
  How the voice will be created. One of `instant`, `professional`, `design`, or `remix`. This is
  informational metadata on the voice - the value is **not** currently plan-gated (for example,
  `professional` is accepted on any plan).
</ParamField>

<ParamField body="model" type="string">
  The synthesis model to bind to the voice. Fixed at creation; an unknown value falls back to the
  default model.
</ParamField>

<Note>
  `visibility` is **not accepted** on the public API. New voices are always created as `private` -
  any `visibility` value sent here is ignored. Publishing a voice is admin-only.
</Note>

<ParamField body="language" type="string" default="en">
  Primary language (BCP-47, e.g. `en`, `hi`). Used when a synthesis request omits `language`.
</ParamField>

<ParamField body="labels" type="object">
  Optional key/value tags, e.g. `{ "accent": "american", "gender": "female" }`.
</ParamField>

#### Response

<ResponseField name="data" type="Voice">
  The created [Voice object](/voice-api/voice-object) with status `draft`.
</ResponseField>

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices" \
    -H "X-API-Key: $FLAM_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"name":"Studio Narrator","language":"en","labels":{"accent":"american"}}'
  ```

  ```javascript JavaScript theme={null}
  const res = await fetch(
    `https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices`,
    {
      method: "POST",
      headers: {
        "X-API-Key": process.env.FLAM_API_KEY,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ name: "Studio Narrator", language: "en", labels: { accent: "american" } }),
    },
  );
  const { data } = await res.json();
  console.log(data.id, data.status);
  ```

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

  res = requests.post(
      f"https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices",
      headers={"X-API-Key": os.environ["FLAM_API_KEY"], "Content-Type": "application/json"},
      json={"name": "Studio Narrator", "language": "en", "labels": {"accent": "american"}},
  )
  print(res.json()["data"])
  ```
</CodeGroup>

```json Response theme={null}
{
  "status": 201,
  "data": {
    "id": "665f1a2b3c4d5e6f7a8b9c0d",
    "name": "Studio Narrator",
    "status": "draft",
    "primary_language": "en",
    "visibility": "private",
    "clone_method": "instant",
    "labels": [{ "key": "accent", "value": "american" }]
  },
  "error": false
}
```

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

WHAT TO BUILD
- A function createVoice(input) (name it per the project's conventions) that creates a new, empty
  voice in the API key's workspace and returns the created Voice.
- 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.

WHAT THIS ENDPOINT DOES
- It creates a voice with status "draft". A draft voice has no audio and CANNOT synthesize until a
  reference clip is enrolled with POST /flam-voice-svc/api/v1/public/voices/{id}/enroll (or you use the one-shot
  POST /flam-voice-svc/api/v1/public/voices/clone instead).

ENDPOINT
- Method: POST
- URL: https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices
- Content-Type: application/json

REQUEST BODY
- name (string, required): display name. Validate it is non-empty locally.
- description (string, optional): long-form description.
- clone_method (string, optional, default "instant"): one of "instant", "professional", "design",
  "remix". Informational metadata only; not plan-gated.
- model (string, optional): synthesis model to bind to the voice. Fixed at creation; an unknown
  value falls back to the default model.
- language (string, optional, default "en"): BCP-47 primary language (e.g. "en", "hi"). Becomes
  the voice's primary_language, used when a synthesis request omits language.
- labels (object, optional): flat string-to-string map, e.g. { "accent": "american" }.
  Note the asymmetry: you SEND labels as an object, but the response returns them as an array of
  { key, value } pairs.
- Do NOT send visibility. It is not accepted on the public API and is ignored; new voices are
  always "private". Leave it out of the input type.
- Omit optional fields that the caller did not provide.

    {
      "name": "Studio Narrator",
      "language": "en",
      "labels": { "accent": "american" }
    }

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.

RESPONSE ENVELOPE
JSON responses use one envelope. On success "error" is false and the payload is in "data":

    {
      "status": 200,
      "data": { "...": "endpoint payload" },
      "error": false
    }

On failure "error" is true, "data" is omitted and "message" carries the detail:

    {
      "status": 404,
      "error": true,
      "message": "Voice not found"
    }

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.

SUCCESS RESPONSE (201, application/json)
"data" is the created Voice, with status "draft" and visibility "private":

    {
      "status": 201,
      "data": {
        "id": "665f1a2b3c4d5e6f7a8b9c0d",
        "name": "Studio Narrator",
        "status": "draft",
        "primary_language": "en",
        "visibility": "private",
        "clone_method": "instant",
        "labels": [{ "key": "accent", "value": "american" }]
      },
      "error": false
    }

VOICE OBJECT (define a type for it; timestamps are Unix epoch MILLISECONDS)
- id: string. Unique voice id, 24-character hex. Used by the synthesize endpoints.
- name: string. Display name.
- description: string, optional.
- status: string. Only "ready" voices can synthesize. A voice being created reports "processing",
  "failed" if creation did not succeed, and a newly created voice with no audio yet is "draft".
  Type it as a string union of these values but tolerate other strings (treat them as not usable).
- clone_method: "instant" | "professional" | "design" | "remix".
- visibility: "private" | "org" | "public".
- primary_language: string, BCP-47 (e.g. "en"). Used when a synthesis request omits language.
- labels: array of { key: string, value: string }, optional. Common keys: accent, gender, age,
  use_case. Example: [{ "key": "accent", "value": "american" }]
- settings: object, optional, present only when configured. Synthesis defaults: stability,
  similarity, style, speed, pitch, volume, use_speaker_boost, emotion, engine_params.
- sample_audio_url: string, optional. Public URL of a short preview clip.
- base_voice_id: string, optional. Present when derived from another voice (clone_method "remix").
- created_at, updated_at: integers (epoch ms).
Optional fields may be absent. Responses may contain extra fields: ignore them, never fail on them.

Example:

    {
      "id": "665f1a2b3c4d5e6f7a8b9c0d",
      "name": "Studio Narrator",
      "description": "Warm, neutral narration voice.",
      "status": "ready",
      "clone_method": "instant",
      "visibility": "public",
      "primary_language": "en",
      "labels": [
        { "key": "accent", "value": "american" },
        { "key": "gender", "value": "female" }
      ],
      "sample_audio_url": "https://storage.googleapis.com/.../sample.wav",
      "created_at": 1717939200000,
      "updated_at": 1717939260000
    }

LIMITS AND BILLING
- Creating a voice counts against the plan's max_voices limit. Over the cap the request returns
  403 with the flat body { "error": "plan limit reached", "feature": "max_voices", "limit": <n> }.
  Surface it as a plan-limit error that includes the limit. Deleting a voice frees a slot.

ERRORS
- 400: invalid request, e.g. missing name or an invalid clone_method. Do not retry.
- 401: X-API-Key missing, malformed, invalid or revoked. Do not retry.
- 403: plan limit reached (flat body above). Do not retry.
- 429: rate limited. 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
- This POST creates a resource, so do NOT retry it automatically after a timeout or 5xx: the first
  attempt may have succeeded and a retry could create a duplicate. Surface the error instead.
- A 429 (rate limited) means the request was refused, so retrying it with exponential backoff plus
  jitter (about 1s, 2s, 4s; at most 3 retries) is safe.
- Never retry 400, 401, 402, 403, 404 or 422.

ACCEPTANCE CRITERIA
- createVoice sends POST with a JSON body containing only the supported fields, never visibility.
- It returns the typed Voice from "data" on 201.
- The max_voices 403 is exposed as a distinct error with its limit; the flat body is parsed
  correctly even though it has no envelope.
- A timeout or 5xx is not retried automatically.
- A missing FLAM_API_KEY fails before any network call. The key is never logged.

TEST PLAN
- Unit tests with mocked HTTP: 201 returns a draft Voice; the body contains no visibility key and
  no undefined optional keys; 400 envelope; 403 plan-limit flat body; 429 then 201 (retry); a
  502 is NOT retried.
- Smoke script: create a voice named "smoke-test-<timestamp>", print its id and status (expect
  "draft"), then clean up with DELETE https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices/{id} (same X-API-Key header; success
  is { "data": { "deleted": true } }) so the test does not consume a max_voices slot.

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.