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

# Enroll a voice

> Attach a reference clip to an existing voice and clone it.

Enrolls an existing voice (typically one you just [created](/voice-api/create-voice) with status
`draft`) from a reference audio clip. Enrollment runs asynchronously: the response is `202 Accepted`
with the `processing` voice and a `job_id` you poll with [`GET /jobs/{id}`](/voice-api/get-job). When
the job reaches `succeeded`, the voice becomes `ready`. A clip the cloner can't use fails the job
rather than the request - the reference needs at least \~6 seconds of continuous speech.

<Note>
  Prefer [`/clone`](/voice-api/clone-voice) if you want to create and enroll a voice in one call. Like
  cloning, enrollment is **free** - no character usage is charged.
</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 to enroll.
</ParamField>

#### Body (`multipart/form-data`)

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

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

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

#### Response

<ResponseField name="data.voice" type="Voice">
  The [Voice object](/voice-api/voice-object), now 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/665f1a2b3c4d5e6f7a8b9c0d/enroll" \
    -H "X-API-Key: $FLAM_API_KEY" \
    -F "language=en" \
    -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 id = "665f1a2b3c4d5e6f7a8b9c0d";
  const form = new FormData();
  form.set("language", "en");
  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/${id}/enroll`,
    { method: "POST", headers: { "X-API-Key": process.env.FLAM_API_KEY }, body: form },
  );
  const { data } = await res.json();
  console.log(data.voice.status, data.job_id);
  ```

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

  voice_id = "665f1a2b3c4d5e6f7a8b9c0d"
  with open("sample.wav", "rb") as f:
      res = requests.post(
          f"https://fi.production.flamapis.com/flam-voice-svc/api/v1/public/voices/{voice_id}/enroll",
          headers={"X-API-Key": os.environ["FLAM_API_KEY"]},
          data={"language": "en", "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"]["status"], data["job_id"])
  ```
</CodeGroup>

```json Response theme={null}
{
  "status": 202,
  "data": {
    "voice": {
      "id": "665f1a2b3c4d5e6f7a8b9c0d",
      "name": "Studio Narrator",
      "status": "processing",
      "primary_language": "en"
    },
    "job_id": "665f1a2b3c4d5e6f7a8b9aa01"
  },
  "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: "Enroll a voice".
Everything you need is in this prompt. Do not invent anything that is not specified here.

WHAT TO BUILD
- A function enrollVoice(voiceId, input) (name it per the project's conventions) that attaches a
  reference clip to an existing voice (typically a "draft" voice from POST /flam-voice-svc/api/v1/public/voices) and
  starts cloning it, 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/{id}/enroll
- Body: multipart/form-data
- Enrollment is asynchronous: the response is 202 Accepted with the voice (now "processing") and a
  job_id to poll. When the job reaches "succeeded", the voice becomes "ready".

PARAMETERS
- id (path, string, required): the voice id to enroll, a 24-character hex string. Validate it
  locally against /^[0-9a-f]{24}$/i and URL-encode it. The voice must belong to the key's
  workspace.

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.
- file (file, required): the reference audio clip. Maximum 20 MiB (20 * 1024 * 1024 bytes);
  larger uploads return 400, so check the size locally before uploading. Send a filename.
- language (text, optional): BCP-47 code of the reference clip (e.g. "en", "hi").
- ref_text (text, optional): transcript of the reference clip. Improves clone quality.
- 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/665f1a2b3c4d5e6f7a8b9c0d/enroll" \
      -H "X-API-Key: $FLAM_API_KEY" \
      -F "language=en" \
      -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": "Studio Narrator",
          "status": "processing",
          "primary_language": "en"
        },
        "job_id": "665f1a2b3c4d5e6f7a8b9aa0"
      },
      "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
    }

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.

BILLING
- Enrollment is free: no character usage is charged.
- To create and enroll in one call there is POST /flam-voice-svc/api/v1/public/voices/clone; do not build it here
  unless asked.

ERRORS
- 400: invalid request, e.g. 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.
- 404: the voice does not exist or belongs to another workspace. Do not retry.
- 422: the voice provider rejected the input, e.g. a clip that is too short. Do not retry.
- 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
- Do NOT retry the enroll POST automatically after a timeout or 5xx: the first attempt may already
  have started a job. Retry it only on 429, with exponential backoff plus jitter (at most 3 tries).
- Poll requests are safe GETs: retry them on 429, 502 and network errors.
- Never retry 400, 401, 402, 403, 404 or 422.

ACCEPTANCE CRITERIA
- enrollVoice validates the id, checks the 20 MiB limit before upload, and sends only the
  documented multipart fields.
- On 202 it returns the typed voice and jobId; waitForJob resolves on "succeeded" and rejects on
  "failed" (with data.error), "cancelled", "expired" or timeout.
- 404 is a distinct, non-retried not-found error. The key is never logged.

TEST PLAN
- Unit tests with mocked HTTP: 404; 202 returns voice + jobId; oversized file
  rejected locally; bad id rejected locally; waitForJob success and failure paths; poll retry on
  502.
- Smoke script: given FLAM_VOICE_ID (a draft voice you own) and ./sample.wav, call enrollVoice.
  Poll and print the final job status.

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.