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

# Clone a voice

> Create a voice and enroll it from a reference clip in a single call.

A convenience one-shot that [creates a voice](/voice-api/create-voice) and then enrolls it from a
reference audio clip in a single `multipart/form-data` request. Enrollment runs asynchronously: the
response is `202 Accepted` with the new (`processing`) voice and a `job_id` you poll with
[`GET /jobs/{id}`](/voice-api/get-job). Once the job reaches `succeeded`, the voice's status becomes
`ready` and it can [synthesize](/voice-api/synthesize-speech).

<Note>
  **Cloning is free** - it accrues no character usage and is never charged. It's bounded only by 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 (`multipart/form-data`)

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

<ParamField body="file" type="file" required>
  The reference audio clip to clone from. Maximum **20 MiB**; larger uploads return `400`.
</ParamField>

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

<ParamField body="language" type="string">
  BCP-47 language code of the reference clip (e.g. `en`, `hi`). Also becomes the voice's primary
  language.
</ParamField>

<ParamField body="labels" type="string">
  Optional key/value tags as a **JSON-encoded string**, e.g.
  `labels={"accent":"british","gender":"male"}`.
</ParamField>

<ParamField body="ref_text" type="string">
  Optional transcript of the reference clip. Improves clone quality when provided.
</ParamField>

<ParamField body="remove_background_noise" type="boolean" default="false">
  Accepts `true`/`1`/`on`/`yes`. When set, the voice is tagged with a
  `background_noise_removal` label. This records the denoise preference as voice
  metadata; the enrollment pipeline does not currently apply denoising.
</ParamField>

#### Response

<ResponseField name="data.voice" type="Voice">
  The new [Voice object](/voice-api/voice-object), initially with status `processing`.
</ResponseField>

<ResponseField name="data.job_id" type="string">
  The clone job id. Poll [`GET /jobs/{id}`](/voice-api/get-job) until it reaches a terminal status.
</ResponseField>

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices/clone" \
    -H "X-API-Key: $FLAM_API_KEY" \
    -F "name=Narrator" \
    -F "description=Warm documentary voice" \
    -F "language=en" \
    -F 'labels={"accent":"british","gender":"male"}' \
    -F "ref_text=The quick brown fox jumps over the lazy dog." \
    -F "file=@./sample.wav"
  ```

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

  const form = new FormData();
  form.set("name", "Narrator");
  form.set("language", "en");
  form.set("labels", JSON.stringify({ accent: "british", gender: "male" }));
  form.set("ref_text", "The quick brown fox jumps over the lazy dog.");
  form.set("file", new Blob([await readFile("./sample.wav")]), "sample.wav");

  const res = await fetch(
    `https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices/clone`,
    { method: "POST", headers: { "X-API-Key": process.env.FLAM_API_KEY }, body: form },
  );
  const { data } = await res.json();
  console.log(data.voice.id, data.job_id);
  ```

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

  with open("sample.wav", "rb") as f:
      res = requests.post(
          f"https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices/clone",
          headers={"X-API-Key": os.environ["FLAM_API_KEY"]},
          data={
              "name": "Narrator",
              "language": "en",
              "labels": json.dumps({"accent": "british", "gender": "male"}),
              "ref_text": "The quick brown fox jumps over the lazy dog.",
          },
          files={"file": ("sample.wav", f, "audio/wav")},
      )
  data = res.json()["data"]
  print(data["voice"]["id"], data["job_id"])
  ```
</CodeGroup>

```json Response theme={null}
{
  "status": 202,
  "data": {
    "voice": {
      "id": "665f1a2b3c4d5e6f7a8b9c0d",
      "name": "Narrator",
      "status": "processing",
      "primary_language": "en"
    },
    "job_id": "665f1a2b3c4d5e6f7a8b9aa01"
  },
  "error": false
}
```

## Poll the clone job

After a successful `202`, poll the job until it reaches a terminal status (`succeeded`, `failed`,
`cancelled`, or `expired`). See [Get a job](/voice-api/get-job). A clip the cloner can't use fails
the job rather than the request - the reference needs at least \~6 seconds of continuous speech.

```bash cURL theme={null}
# 1. Kick off the clone and capture the job id.
JOB_ID=$(curl -s -X POST "https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices/clone" \
  -H "X-API-Key: $FLAM_API_KEY" \
  -F "name=Narrator" \
  -F "language=en" \
  -F "file=@./sample.wav" | jq -r '.data.job_id')

# 2. Poll until the job finishes.
while :; do
  STATUS=$(curl -s "https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/jobs/$JOB_ID" \
    -H "X-API-Key: $FLAM_API_KEY" | jq -r '.data.status')
  echo "job status: $STATUS"
  case "$STATUS" in
    succeeded) echo "voice is ready"; break ;;
    failed|cancelled|expired) echo "clone did not complete"; break ;;
  esac
  sleep 2
done
```

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

WHAT TO BUILD
- A function cloneVoice(input) (name it per the project's conventions) that creates a voice AND
  enrolls it from a reference audio clip in one multipart request, returning { voice, jobId }.
- A function waitForJob(jobId, options) that polls the clone job until it reaches a terminal
  status (reuse an existing one if the project already has 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/clone
- Body: multipart/form-data
- Enrollment is asynchronous: the response is 202 Accepted with the new voice (status
  "processing") and a job_id to poll.

REQUEST FIELDS (multipart/form-data)
- Send multipart/form-data. Do not set the Content-Type header yourself; let the HTTP library
  add it with the multipart boundary.
- name (text, required): display name for the new voice. Validate non-empty locally.
- file (file, required): the reference audio clip to clone from. Maximum 20 MiB (20 * 1024 * 1024
  bytes); larger uploads return 400, so check the size locally before uploading. Accept a file
  path, Buffer/Blob or stream as fits the stack, and send a filename (e.g. "sample.wav").
- description (text, optional): long-form description.
- language (text, optional): BCP-47 code of the reference clip (e.g. "en", "hi"). Also becomes the
  voice's primary language.
- labels (text, optional): key/value tags as a JSON-ENCODED STRING of a flat object, e.g.
  labels={"accent":"british","gender":"male"}. Take an object in your input type and
  JSON.stringify it yourself.
- ref_text (text, optional): transcript of the reference clip. Improves clone quality.
- remove_background_noise (text, optional, default false): accepts "true", "1", "on" or "yes".
  When set, the voice is tagged with a background_noise_removal label. This only records the
  preference as metadata; the pipeline does not currently apply denoising, so do not promise it.
- Omit optional fields that the caller did not provide.

Equivalent curl:

    curl -X POST "https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices/clone" \
      -H "X-API-Key: $FLAM_API_KEY" \
      -F "name=Narrator" \
      -F "language=en" \
      -F 'labels={"accent":"british","gender":"male"}' \
      -F "ref_text=The quick brown fox jumps over the lazy dog." \
      -F "file=@./sample.wav"

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 (202, application/json)

    {
      "status": 202,
      "data": {
        "voice": {
          "id": "665f1a2b3c4d5e6f7a8b9c0d",
          "name": "Narrator",
          "status": "processing",
          "primary_language": "en"
        },
        "job_id": "665f1a2b3c4d5e6f7a8b9aa0"
      },
      "error": false
    }

- data.voice: the new Voice, initially "processing".
- data.job_id: the clone job id to poll.

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
    }

JOB OBJECT (returned by GET https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/jobs/{id} in "data")
- id: string. The job id.
- voice_id: string. The voice this job is enrolling.
- org_id: string. The owning workspace.
- job_type: string, e.g. "instant_clone", "pro_train", "verify", "localize", "design_preview",
  "reembed".
- status: "pending" | "running" while in progress; terminal states are "succeeded", "failed",
  "cancelled" and "expired". When "succeeded", the voice's status is "ready".
- progress: integer 0-100.
- error: object, present when status is "failed"; carries the provider error detail.

    {
      "status": 200,
      "data": {
        "id": "665f1a2b3c4d5e6f7a8b9aa0",
        "voice_id": "665f1a2b3c4d5e6f7a8b9c0d",
        "org_id": "664a0b1c2d3e4f5a6b7c8d9e",
        "job_type": "instant_clone",
        "status": "succeeded",
        "progress": 100
      },
      "error": false
    }

POLLING THE JOB
- Poll GET https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/jobs/{job_id} (same X-API-Key header) every ~2 seconds until
  data.status is terminal: "succeeded", "failed", "cancelled" or "expired". "pending" and "running"
  mean keep polling.
- On "succeeded" the voice is "ready" and can synthesize. On "failed" surface data.error (for
  example, a clip without at least ~6 seconds of continuous speech fails the job, not the POST). On
  "cancelled" or "expired" surface a clear "clone did not complete" error.
- Make the interval and an overall timeout configurable, accept an abort/cancel signal, and retry
  individual poll requests on 429, 502 and network errors with backoff (a poll is a safe GET).
- A 404 while polling means the job does not exist in this workspace: stop and surface it.

LIMITS AND BILLING
- Cloning is free: it accrues no character usage and is never charged.
- It is bounded by the plan's max_voices limit. Over the cap: 403 with the flat body
  { "error": "plan limit reached", "feature": "max_voices", "limit": <n> }.

ERRORS
- 400: invalid request, e.g. missing name, missing file, or a file over 20 MiB. Do not retry.
- 401: X-API-Key missing, malformed, invalid or revoked. Do not retry.
- 402: metered allowance exhausted (flat billing body). Do not retry.
- 403: "max_voices" plan limit reached (flat body with "feature" and "limit"). Do not retry.
- 422: the voice provider rejected the input, e.g. a reference clip that is too short
  (message such as "audio_too_short"). Do not retry; tell the caller to fix the clip.
- 429: rate limited. 502: provider temporarily unavailable.
- 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
- cloneVoice sends a multipart request with exactly the documented fields, labels as a JSON
  string, and a file no larger than 20 MiB (checked before upload).
- On 202 it returns the typed voice and jobId; waitForJob resolves on "succeeded" and rejects with
  a descriptive error on "failed", "cancelled", "expired" or its own timeout.
- The 403 max_voices answer is a distinct, non-retried plan-limit error exposing "limit".
- The POST is never auto-retried after a timeout or 5xx. The key is never logged.

TEST PLAN
- Unit tests with mocked HTTP: 403 max_voices maps to a plan-limit error with limit; 202 returns voice + jobId; a 21 MiB file is rejected
  locally; labels are serialized as a JSON string; waitForJob walks running -> succeeded, and
  running -> failed surfaces data.error; a poll 502 is retried.
- Smoke script: call cloneVoice with a local sample.wav, poll the job and print the final status
  and the voice id.

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.